diff --git a/.github/ISSUE_TEMPLATE/问题反馈.md b/.github/ISSUE_TEMPLATE/问题反馈.md
deleted file mode 100644
index 9c95983..0000000
--- a/.github/ISSUE_TEMPLATE/问题反馈.md
+++ /dev/null
@@ -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`的输出,以及其他相关信息,以及你的建议
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 121d32a..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -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"
diff --git a/.github/workflows/ISSUE_TEMPLATE.md b/.github/workflows/ISSUE_TEMPLATE.md
deleted file mode 100644
index b91c501..0000000
--- a/.github/workflows/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# 问题反馈
-
-## **请确保**
-
-- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
-- 你的问题是在最新版本的代码上测试的
-- 请勿重复提交相同或类似的issue
-
-
-## **描述问题**
-
-请在此简单描述问题
-
-
-
-## **如何复现**
-
-请阐述一下如何重现这个问题
-### 预期
-
-描述你期望发生的事情
-
-### 实际
-
-描述实际发生的事情
-
-
-
-## **日志或截图**
-```
-日志内容
-```
-
-
-## **设备信息**
-- **系统**: [例如 Ubuntu 22.04]
-- **CPU**: [例如 Intel i7-7700K]
-- **内存**: [例如 16GB]
-- **Python**: [例如CPython 3.10.7]
-
-
-**补充内容**
-
-可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
deleted file mode 100644
index 54c9858..0000000
--- a/.github/workflows/deploy-docs.yml
+++ /dev/null
@@ -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
diff --git a/.gitignore b/.gitignore
index a640ab8..86a5a45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ __pycache__/
*.pyd
*.pyw
/plugins/
+
+#config
+/config/
_config.yml
config.yml
config.example.yml
@@ -29,11 +32,11 @@ src/nonebot_plugins/dislink_plugin_ccnd
.github
# pyproject.toml
-test.py
-line_count.py
+# mypy
mypy.ini
# nuitka
+compile.bat
main.build/
main.dist/
main.exe
@@ -46,3 +49,23 @@ prompt.txt
# js
**/echarts.js
.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
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 58839fe..fcd1d16 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/README.md b/README.md
index a6c2fd1..b02452d 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
[![][Python3.10+]][python-link]
[![][Usage]][usage-link]
-- 基于[Nonebot2](https://github.com/nonebot/nonebot2),有良好的生态支持
+- 原生支持与任意 `Python`Bot 框架互联,有良好的生态支持
- 开箱即用,无需复杂配置
- 集成包管理器,支持一键安装插件
- 支持 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)提供的技术指导和服务器资源
- 特别感谢[云裳工作室](https://doc.ysmcc.cn/doc/1/)提供的服务器挂载
- 由衷感谢我在学习生活中遇到的所有朋友们,你们身为我生命中的一处景色,不断地推进我此生的进程。
diff --git a/clean_pycache.py b/clean_pycache.py
new file mode 100644
index 0000000..eeb8efe
--- /dev/null
+++ b/clean_pycache.py
@@ -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()
diff --git a/liteyuki/__init__.py b/liteyuki/__init__.py
index 4ace66e..c96f3ed 100644
--- a/liteyuki/__init__.py
+++ b/liteyuki/__init__.py
@@ -1,11 +1,12 @@
from liteyuki.bot import (
LiteyukiBot,
- get_bot
+ get_bot,
+ get_config,
+ get_config_with_compat
)
from liteyuki.comm import (
Channel,
- chan,
Event
)
@@ -15,7 +16,27 @@ from liteyuki.plugin 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. 优化了通道的性能
diff --git a/liteyuki/bot/__init__.py b/liteyuki/bot/__init__.py
index 3f81fbf..0d0dc13 100644
--- a/liteyuki/bot/__init__.py
+++ b/liteyuki/bot/__init__.py
@@ -1,103 +1,102 @@
import asyncio
+import atexit
import os
import platform
+import signal
import sys
import threading
import time
from typing import Any, Optional
-from watchdog.events import FileSystemEventHandler
-from watchdog.observers import Observer
-
-from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan
-from liteyuki.core import IS_MAIN_PROCESS
+from liteyuki.bot.lifespan import LIFESPAN_FUNC, Lifespan, PROCESS_LIFESPAN_FUNC
+from liteyuki.comm.channel import get_channel
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.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:
- def __init__(self, *args, **kwargs):
+ def __init__(self, **kwargs) -> None:
+ """
+ 初始化轻雪实例
+ Args:
+ **kwargs: 配置
+ """
+ """常规操作"""
+ print_logo()
global _BOT_INSTANCE
_BOT_INSTANCE = self # 引用
+
+ """配置"""
self.config: dict[str, Any] = kwargs
+
+ """初始化"""
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()
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
+ """加载插件加载器"""
+ 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):
- load_plugins("liteyuki/plugins") # 加载轻雪插件
+ """
+ 外部启动接口
+ """
+ self.process_manager.start_all()
+ try:
+ asyncio.run(self._run())
+ except KeyboardInterrupt:
+ logger.opt(colors=True).info("尹灵温 关闭中…")
+ self.stop()
+ logger.opt(colors=True).info("尹灵温 已关停")
- self.loop_thread.start() # 启动事件循环
- asyncio.run(self.lifespan.before_start()) # 启动前钩子
-
- self.process_manager.add_target("nonebot", nb_run, **self.config)
- self.process_manager.start("nonebot")
-
- self.process_manager.add_target("melobot", mb_run, **self.config)
- self.process_manager.start("melobot")
-
- asyncio.run(self.lifespan.after_start()) # 启动后钩子
-
- 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()
+ async def keep_alive(self):
+ """
+ 保持轻雪运行
+ """
+ logger.info("尹灵温 持续运行中…")
+ try:
+ while not self.stop_event.is_set():
+ await asyncio.sleep(0.1)
+ except Exception:
+ logger.info("尹灵温 现退停…")
+ self.stop()
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:
executable = sys.executable
args = sys.argv
- logger.info("正在重启 尹灵温...")
+ logger.info("正在重启 尹灵温机器人框架")
time.sleep(delay)
if platform.system() == "Windows":
cmd = "start"
@@ -110,7 +109,9 @@ class LiteyukiBot:
self.process_manager.terminate_all()
# 进程退出后重启
threading.Thread(
- target=os.system, args=(f"{cmd} {executable} {' '.join(args)}",)
+ target=os.system,
+ args=(f"{cmd} {executable} {' '.join(args)}",),
+ daemon=True,
).start()
sys.exit(0)
self.call_restart_count += 1
@@ -119,44 +120,46 @@ class LiteyukiBot:
"""
停止轻雪
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:
-
"""
- logger.info("Stopping LiteyukiBot...")
-
- self.loop.create_task(self.lifespan.before_shutdown()) # 重启前钩子
- self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
-
- if name:
- self.process_manager.terminate(name)
+ if name is not None:
+ chan_active = get_channel(f"{name}-active")
+ chan_active.send(1)
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):
"""
初始化轻雪, 自动调用
- Returns:
-
+ Args:
+ *args: 参数
+ **kwargs: 关键字参数
"""
- self.init_config()
self.init_logger()
def init_logger(self):
- # 修改nonebot的日志配置
+ """
+ 初始化日志
+ """
init_log(config=self.config)
- def init_config(self):
- pass
+ def stop(self):
+ """
+ 停止轻雪
+ """
+ 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:
- func:
-
+ func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
Returns:
-
+ [`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
"""
return self.lifespan.on_before_start(func)
@@ -164,81 +167,128 @@ class LiteyukiBot:
"""
注册启动后的函数
Args:
- func:
-
+ func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
Returns:
-
+ [`LIFESPAN_FUNC`](./lifespan#var-lifespan-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):
"""
注册停止后的函数:未实现
Args:
- func:
-
+ func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
Returns:
-
+ [`LIFESPAN_FUNC`](./lifespan#var-lifespan-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:
- func:
-
+ func ([`PROCESS_LIFESPAN_FUNC`](./lifespan#var-process-lifespan-func)): 生命周期函数
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):
"""
注册重启后的函数:未实现
Args:
- func:
-
+ func ([`LIFESPAN_FUNC`](./lifespan#var-lifespan-func)): 生命周期函数
Returns:
-
+ [`LIFESPAN_FUNC`](./lifespan#var-lifespan-func): 生命周期函数
"""
return self.lifespan.on_after_restart(func)
- def on_after_nonebot_init(self, func: LIFESPAN_FUNC):
- """
- 注册nonebot初始化后的函数
- Args:
- func:
- Returns:
-
- """
- return self.lifespan.on_after_nonebot_init(func)
+_BOT_INSTANCE: LiteyukiBot
-_BOT_INSTANCE: Optional[LiteyukiBot] = None
-
-
-def get_bot() -> Optional[LiteyukiBot]:
+def get_bot() -> LiteyukiBot:
"""
获取轻雪实例
Returns:
- LiteyukiBot: 当前的轻雪实例
+ [`LiteyukiBot`](#class-liteyukibot): 轻雪实例
"""
+
if IS_MAIN_PROCESS:
+ if _BOT_INSTANCE is None:
+ raise RuntimeError("尹灵温 实例未初始化")
return _BOT_INSTANCE
else:
- # 从多进程上下文中获取
- pass
+ raise RuntimeError("无法在子进程中获取机器人实例")
+
+
+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"
+ )
diff --git a/liteyuki/bot/lifespan.py b/liteyuki/bot/lifespan.py
index 25c0b08..50e5681 100644
--- a/liteyuki/bot/lifespan.py
+++ b/liteyuki/bot/lifespan.py
@@ -8,14 +8,19 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : lifespan.py
@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.utils import is_coroutine_callable
+from liteyuki.utils import is_coroutine_callable, async_wrapper
-SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any]
-ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]]
-LIFESPAN_FUNC: TypeAlias = SYNC_LIFESPAN_FUNC | ASYNC_LIFESPAN_FUNC
+SYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Any] # 同步生命周期函数
+ASYNC_LIFESPAN_FUNC: TypeAlias = Callable[[], Awaitable[Any]] # 异步生命周期函数
+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:
@@ -23,41 +28,35 @@ class Lifespan:
"""
轻雪生命周期管理,启动、停止、重启
"""
-
- self.life_flag: int = 0 # 0: 启动前,1: 启动后,2: 停止前,3: 停止后
+ self.life_flag: int = 0
self._before_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._before_restart_funcs: list[LIFESPAN_FUNC] = []
+ self._before_process_restart_funcs: list[PROCESS_LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
- self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
-
@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:
- 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:
"""
- for func in funcs:
- if is_coroutine_callable(func):
- await func()
- else:
- func()
+ tasks = [func(*args, **kwargs) if is_coroutine_callable(func) else async_wrapper(func)(*args, **kwargs) for func in funcs]
+ await asyncio.gather(*tasks)
def on_before_start(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册启动时的函数
Args:
- func:
+ func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
Returns:
- LIFESPAN_FUNC:
+ [`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
"""
self._before_start_funcs.append(func)
return func
@@ -66,124 +65,95 @@ class Lifespan:
"""
注册启动时的函数
Args:
- func:
+ func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
Returns:
- LIFESPAN_FUNC:
+ [`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
"""
self._after_start_funcs.append(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:
- func:
+ func ([`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)): 进程生命周期函数
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
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止后的函数
Args:
- func:
-
+ func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
Returns:
- LIFESPAN_FUNC:
-
+ [`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
"""
self._after_shutdown_funcs.append(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:
- func:
+ func ([`PROCESS_LIFESPAN_FUNC`](#var-process-lifespan-func)): 进程生命周期函数
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
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启后的函数
Args:
- func:
+ func ([`LIFESPAN_FUNC`](#var-lifespan-func)): 生命周期函数
Returns:
- LIFESPAN_FUNC:
+ [`LIFESPAN_FUNC`](#var-lifespan-func): 生命周期函数
"""
self._after_restart_funcs.append(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:
"""
- 启动前
- Returns:
+ 启动前钩子
"""
- logger.debug("正在运行 before_start 之函数")
- await self._run_funcs(self._before_start_funcs)
+ logger.debug("运行 before_start 函数")
+ await self.run_funcs(self._before_start_funcs)
async def after_start(self) -> None:
"""
- 启动后
- Returns:
+ 启动后钩子
"""
- logger.debug("正在运行 after_start 之函数")
- await self._run_funcs(self._after_start_funcs)
+ logger.debug("运行 after_start 函数")
+ 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 之函数")
- await self._run_funcs(self._before_shutdown_funcs)
+ logger.debug("运行 before_shutdown 函数")
+ await self.run_funcs(self._before_process_shutdown_funcs, *args, **kwargs)
async def after_shutdown(self) -> None:
"""
- 停止后
- Returns:
+ 停止后钩子 未实现
"""
- logger.debug("正在运行 after_shutdown 之函数")
- await self._run_funcs(self._after_shutdown_funcs)
+ logger.debug("运行 after_shutdown 函数")
+ 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 之函数")
- await self._run_funcs(self._before_restart_funcs)
+ logger.debug("运行 before_restart 函数")
+ await self.run_funcs(self._before_process_restart_funcs, *args, **kwargs)
async def after_restart(self) -> None:
"""
- 重启后
- Returns:
-
+ 重启后钩子 未实现
"""
- logger.debug("正在运行 after_restart 之函数")
- 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)
+ logger.debug("运行 after_restart 函数")
+ await self.run_funcs(self._after_restart_funcs)
diff --git a/liteyuki/comm/__init__.py b/liteyuki/comm/__init__.py
index ac4c6a7..e4bab9a 100644
--- a/liteyuki/comm/__init__.py
+++ b/liteyuki/comm/__init__.py
@@ -1,30 +1,38 @@
# -*- 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子进程之间的通信
+依赖关系
+event -> _
+storage -> channel_
+rpc -> channel_, storage
"""
from liteyuki.comm.channel import (
Channel,
- chan,
get_channel,
set_channel,
set_channels,
- get_channels
+ get_channels,
+ active_channel,
+ passive_channel
)
from liteyuki.comm.event import Event
__all__ = [
"Channel",
- "chan",
"Event",
"get_channel",
"set_channel",
"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
diff --git a/liteyuki/comm/channel.py b/liteyuki/comm/channel.py
index ec59edb..677f7f6 100644
--- a/liteyuki/comm/channel.py
+++ b/liteyuki/comm/channel.py
@@ -1,219 +1,305 @@
# -*- 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 multiprocessing
-import threading
+import asyncio
from multiprocessing import Pipe
-from typing import Any, Optional, Callable, Awaitable, List, TypeAlias
-from uuid import uuid4
+from typing import Any, Callable, Coroutine, Generic, Optional, TypeAlias, TypeVar, get_args
-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]
-ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[Any], Awaitable[Any]]
-ON_RECEIVE_FUNC: TypeAlias = SYNC_ON_RECEIVE_FUNC | ASYNC_ON_RECEIVE_FUNC
+T = TypeVar("T")
-SYNC_FILTER_FUNC: TypeAlias = Callable[[Any], bool]
-ASYNC_FILTER_FUNC: TypeAlias = Callable[[Any], Awaitable[bool]]
-FILTER_FUNC: TypeAlias = SYNC_FILTER_FUNC | ASYNC_FILTER_FUNC
+SYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Any] # 同步接收函数
+ASYNC_ON_RECEIVE_FUNC: TypeAlias = Callable[[T], Coroutine[Any, Any, Any]] # 异步接收函数
+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"] = {}
-_callback_funcs: dict[str, ON_RECEIVE_FUNC] = {}
+_callback_funcs: dict[int, ON_RECEIVE_FUNC] = {}
-class Channel:
+class Channel(Generic[T]):
"""
- 通道类,用于进程间通信,进程内不可用,仅限主进程和子进程之间通信
+ 通道类,可以在进程间和进程内通信,双向但同时只能有一个发送者和一个接收者
有两种接收工作方式,但是只能选择一种,主动接收和被动接收,主动接收使用 `receive` 方法,被动接收使用 `on_receive` 装饰器
"""
- def __init__(self, _id: str):
- self.main_send_conn, self.sub_receive_conn = Pipe()
- self.sub_send_conn, self.main_receive_conn = Pipe()
- self._closed = False
- self._on_main_receive_funcs: list[str] = []
- self._on_sub_receive_funcs: list[str] = []
- self.name: str = _id
+ def __init__(self, name: str, type_check: Optional[bool] = None):
+ """
+ 初始化通道
+ Args:
+ name: 通道ID
+ type_check: 是否开启类型检查, 若为空,则传入泛型默认开启,否则默认关闭
+ """
- self.is_main_receive_loop_running = False
- self.is_sub_receive_loop_running = False
+ self.conn_send, self.conn_recv = Pipe()
+ 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):
return f"Channel({self.name})"
- def send(self, data: Any):
+ def send(self, data: T):
"""
- 发送数据
+ 发送数据,发送函数为同步函数,没有异步的必要
Args:
- data: 数据
+ data (T): 数据
"""
- if self._closed:
- raise RuntimeError("无法发送至已关闭的通道中")
- if IS_MAIN_PROCESS:
- print("主进程发送数据:", data)
- self.main_send_conn.send(data)
- else:
- print("子进程发送数据:", data)
- self.sub_send_conn.send(data)
+ if self.type_check:
+ _type = self._get_generic_type()
+ if _type is not None and not self._validate_structure(data, _type):
+ raise TypeError(f"该数据必须为 {_type} 实例,而非 {type(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:
- raise RuntimeError("无法从已关闭的通道中接收")
+ raise RuntimeError("无法在已关闭的 Channel 中接取数据")
while True:
- # 判断receiver是否为None或者receiver是否等于接收者,是则接收数据,否则不动数据
- if IS_MAIN_PROCESS:
- data = self.main_receive_conn.recv()
- print("主进程接收数据:", data)
- else:
- data = self.sub_receive_conn.recv()
- print("子进程接收数据:", data)
-
+ data = self.conn_recv.recv()
return data
- def close(self):
+ async def async_receive(self) -> T:
"""
- 关闭通道
+ 异步接收数据,会挂起等待
+ Returns:
+ T: 数据
"""
- self._closed = True
- self.sub_receive_conn.close()
- self.main_send_conn.close()
- self.sub_send_conn.close()
- self.main_receive_conn.close()
+ loop = asyncio.get_running_loop()
+ data = await loop.run_in_executor(None, self.receive)
+ return data
- 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:
- 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:
- 装饰器,装饰一个函数在接收到数据后执行
+ Callable[[Callable[[T], Any]], Callable[[T], Any]]: 装饰器
"""
- if (not self.is_sub_receive_loop_running) and not IS_MAIN_PROCESS:
- threading.Thread(target=self._start_sub_receive_loop).start()
+ if not IS_MAIN_PROCESS:
+ raise RuntimeError("on_receive 仅可用于主进程内")
- if (not self.is_main_receive_loop_running) and IS_MAIN_PROCESS:
- threading.Thread(target=self._start_main_receive_loop).start()
+ def decorator(func: Callable[[T], Any]) -> Callable[[T], Any]:
+ global _func_id
- def decorator(func: ON_RECEIVE_FUNC) -> ON_RECEIVE_FUNC:
- async def wrapper(data: Any) -> Any:
+ async def wrapper(data: T) -> Any:
if filter_func is not None:
if is_coroutine_callable(filter_func):
- if not await filter_func(data):
+ if not (await filter_func(data)): # type: ignore
return
else:
if not filter_func(data):
return
- return await func(data)
- function_id = str(uuid4())
- _callback_funcs[function_id] = wrapper
+ if is_coroutine_callable(func):
+ return await func(data)
+ else:
+ return func(data)
+
+ _callback_funcs[_func_id] = wrapper
if IS_MAIN_PROCESS:
- self._on_main_receive_funcs.append(function_id)
+ self._on_main_receive_func_ids.append(_func_id)
else:
- self._on_sub_receive_funcs.append(function_id)
+ self._on_sub_receive_func_ids.append(_func_id)
+ _func_id += 1
return func
return decorator
- def _run_on_main_receive_funcs(self, data: Any):
+ async def _run_on_receive_funcs(self, data: Any):
"""
运行接收函数
Args:
data: 数据
"""
- for func_id in self._on_main_receive_funcs:
- func = _callback_funcs[func_id]
- run_coroutine(func(data))
-
- 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()
+ if IS_MAIN_PROCESS:
+ [asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_main_receive_func_ids]
+ else:
+ [asyncio.create_task(_callback_funcs[func_id](data)) for func_id in self._on_sub_receive_func_ids]
-"""默认通道实例,可直接从模块导入使用"""
-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:
- name: 通道名称
- channel: 通道实例
+ name ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 通道名称
+ 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:
- 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():
- _channel[name] = channel
+ set_channel(name, channel)
-def get_channel(name: str) -> Optional[Channel]:
+def get_channel(name: str) -> "Channel":
"""
获取通道实例
Args:
- name: 通道名称
+ name ([`str`](https%3A//docs.python.org/3/library/stdtypes.html#str)): 通道名称
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:
+ [`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()
diff --git a/liteyuki/comm/event.py b/liteyuki/comm/event.py
index c3bddbf..3808b52 100644
--- a/liteyuki/comm/event.py
+++ b/liteyuki/comm/event.py
@@ -1,12 +1,6 @@
# -*- 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
diff --git a/liteyuki/comm/rpc.py b/liteyuki/comm/rpc.py
new file mode 100644
index 0000000..6b46f08
--- /dev/null
+++ b/liteyuki/comm/rpc.py
@@ -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)
diff --git a/liteyuki/comm/socks_channel.py b/liteyuki/comm/socks_channel.py
new file mode 100644
index 0000000..46b8a09
--- /dev/null
+++ b/liteyuki/comm/socks_channel.py
@@ -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
diff --git a/liteyuki/comm/storage.py b/liteyuki/comm/storage.py
new file mode 100644
index 0000000..e717b8e
--- /dev/null
+++ b/liteyuki/comm/storage.py
@@ -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
diff --git a/liteyuki/config.py b/liteyuki/config.py
index 73c6170..c2601a7 100644
--- a/liteyuki/config.py
+++ b/liteyuki/config.py
@@ -1,49 +1,132 @@
+"""
+该模块用于常用配置文件的加载
+多配置文件编写原则:
+1. 尽量不要冲突: 一个键不要多次出现
+2. 分工明确: 每个配置文件给一个或一类服务提供配置
+3. 扁平化编写: 配置文件尽量扁平化,不要出现过多的嵌套
+4. 注意冲突时的优先级: 项目目录下的配置文件优先级高于config目录下的配置文件
+5. 请不要将需要动态加载的内容写入配置文件,你应该使用其他储存方式
+"""
+
import os
-from typing import List
-
-import nonebot
+import json
+import copy
+import toml
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):
- host: str = ""
- port: str = "5500"
- path: str = ""
- token: str = ""
+def load_from_yaml(file_: str) -> dict[str, Any]:
+ """
+ Load config from yaml file
+
+ """
+ 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):
- comment: str = "此皆正处于开发之中,切勿在生产环境中启用。"
- enable: bool = False
- hosts: List[SatoriNodeConfig] = [SatoriNodeConfig()]
+def load_from_json(file_: str) -> dict[str, Any]:
+ """
+ Load config from json file
+ """
+ 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):
- host: str = "127.0.0.1"
- port: int = 20247
- superusers: list[str] = []
- command_start: list[str] = ["/", ""]
- nickname: list[str] = [f"灵温"]
- satori: SatoriConfig = SatoriConfig()
- data_path: str = "data/liteyuki"
+def load_from_toml(file_: str) -> dict[str, Any]:
+ """
+ Load config from toml file
+ """
+ logger.debug("正在从 {} 中加载 TOML 配置".format(file_))
+ config = toml.load(open(file_, "r", encoding="utf-8"))
+ return flat_config(config if config is not None else {})
-def load_from_yaml(file: str) -> dict:
- 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:
- yaml.dump(BasicConfig().dict(), f, default_flow_style=False)
+def load_from_files(*files: str, no_warning: bool = False) -> dict[str, Any]:
+ """
+ 从指定文件加载配置项,会自动识别文件格式
+ 默认执行扁平化选项
+ """
+ config = {}
+ 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)
- config = conf
- if conf is None:
- nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
- conf = BasicConfig().dict()
- return conf
+
+def load_configs_from_dirs(
+ *directories: str, no_waring: bool = False
+) -> dict[str, Any]:
+ """
+ 从目录下加载配置文件,不递归
+ 按照读取文件的优先级反向覆盖
+ 默认执行扁平化选项
+ """
+ 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
diff --git a/liteyuki/core/__init__.py b/liteyuki/core/__init__.py
index f95f814..625d840 100644
--- a/liteyuki/core/__init__.py
+++ b/liteyuki/core/__init__.py
@@ -1,11 +1,2 @@
-import multiprocessing
-
-from .spawn_process import *
from .manager import *
-__all__ = [
- "IS_MAIN_PROCESS"
-]
-
-IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
-
diff --git a/liteyuki/core/manager.py b/liteyuki/core/manager.py
index 944a8dd..6a209b1 100644
--- a/liteyuki/core/manager.py
+++ b/liteyuki/core/manager.py
@@ -9,92 +9,175 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Software: PyCharm
"""
import asyncio
+import multiprocessing
import threading
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.utils import IS_MAIN_PROCESS
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
__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:
"""
- 在主进程中被调用
+ 进程管理器
"""
- def __init__(self, bot: "LiteyukiBot"):
- self.bot = bot
- self.targets: dict[str, tuple[callable, tuple, dict]] = {}
+ def __init__(self, lifespan: "Lifespan"):
+ self.lifespan = lifespan
+ self.targets: dict[str, tuple[Callable, tuple, dict]] = {}
self.processes: dict[str, Process] = {}
- set_channels(
- {
- "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):
+ def _run_process(self, name: str):
"""
- 开启后自动监控进程,并添加到进程字典中
+ 开启后自动监控进程,并添加到进程字典中,会阻塞,请创建task
Args:
name:
- delay:
-
Returns:
-
"""
if name not in self.targets:
- raise KeyError(f"未有 Process {name} 之存在")
+ raise KeyError(f"Process {name} 未寻得")
- def _start():
- 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
+ chan_active = get_channel(f"{name}-active")
- elif data == 0:
- logger.info(f"关停 {name} 进程")
- asyncio.run(self.bot.lifespan.before_shutdown())
- should_exit = True
- self.terminate(name)
- else:
- logger.warning("数据未知,省略:{}".format(data))
+ def _start_process():
+ process = Process(
+ target=self.targets[name][0],
+ args=self.targets[name][1],
+ kwargs=self.targets[name][2],
+ daemon=True,
+ )
+ self.processes[name] = process
+ process.start()
- if delay:
- threading.Timer(delay, _start).start()
- else:
- threading.Thread(target=_start).start()
+ # 启动进程并监听信号
+ _start_process()
+ while True:
+ 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):
- self.targets[name] = (target, args, kwargs)
+ def start_all(self):
+ """
+ 对外启动方法,启动所有进程,创建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:
process.join()
@@ -107,14 +190,29 @@ class ProcessManager:
Returns:
"""
- if name not in self.targets:
- raise logger.warning(f"未有 Process {name} 之存在")
+ if name not in self.processes:
+ logger.warning(f"Process {name} 未寻得")
+ return
process = self.processes[name]
process.terminate()
process.join(TIMEOUT)
if process.is_alive():
process.kill()
+ logger.success(f"Process {name} 已迫令终止")
def terminate_all(self):
for name in self.targets:
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()
diff --git a/liteyuki/core/spawn_process.py b/liteyuki/core/spawn_process.py
deleted file mode 100644
index 040392a..0000000
--- a/liteyuki/core/spawn_process.py
+++ /dev/null
@@ -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()
diff --git a/liteyuki/dev/__init__.py b/liteyuki/dev/__init__.py
new file mode 100644
index 0000000..893c170
--- /dev/null
+++ b/liteyuki/dev/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+"""
+该模块用于存放一些开发工具
+"""
diff --git a/liteyuki/dev/observer.py b/liteyuki/dev/observer.py
new file mode 100644
index 0000000..6be869c
--- /dev/null
+++ b/liteyuki/dev/observer.py
@@ -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
diff --git a/liteyuki/dev/plugin.py b/liteyuki/dev/plugin.py
new file mode 100644
index 0000000..c541bba
--- /dev/null
+++ b/liteyuki/dev/plugin.py
@@ -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()
diff --git a/liteyuki/log.py b/liteyuki/log.py
index a779a14..1e8a92b 100644
--- a/liteyuki/log.py
+++ b/liteyuki/log.py
@@ -9,22 +9,10 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Software: PyCharm
"""
import sys
+
import loguru
-from typing import TYPE_CHECKING
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_format: str = (
@@ -50,35 +38,26 @@ def get_format(level: str) -> str:
return default_format
-logger = loguru.logger.bind()
-
-
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
"""
- global logger
logger.remove()
logger.add(
sys.stdout,
level=0,
diagnose=False,
- filter=default_filter,
format=get_format(config.get("log_level", "INFO")),
)
show_icon = config.get("log_icon", True)
+ logger.level("DEBUG", color="", icon=f"{'🐛' if show_icon else ''}试")
+ logger.level("INFO", color="", icon=f"{'ℹ️' if show_icon else ''}讯")
+ logger.level("SUCCESS", color="", icon=f"{'✅' if show_icon else ''}警")
+ logger.level("WARNING", color="", icon=f"{'⚠️' if show_icon else ''}误")
+ logger.level("ERROR", color="", icon=f"{'⭕' if show_icon else ''}成")
- # debug = lang.get("log.debug", default="==DEBUG")
- # info = lang.get("log.info", default="===INFO")
- # 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="", icon=f"{'🐛' if show_icon else ''}{debug}")
- # logger.level("INFO", color="", icon=f"{'ℹ️' if show_icon else ''}{info}")
- # logger.level("SUCCESS", color="", icon=f"{'✅' if show_icon else ''}{success}")
- # logger.level("WARNING", color="", icon=f"{'⚠️' if show_icon else ''}{warning}")
- # logger.level("ERROR", color="", icon=f"{'⭕' if show_icon else ''}{error}")
+
+init_log(config={})
diff --git a/liteyuki/message/__init__.py b/liteyuki/message/__init__.py
new file mode 100644
index 0000000..8c566fd
--- /dev/null
+++ b/liteyuki/message/__init__.py
@@ -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
+"""
diff --git a/liteyuki/message/event.py b/liteyuki/message/event.py
new file mode 100644
index 0000000..a68a60e
--- /dev/null
+++ b/liteyuki/message/event.py
@@ -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)
diff --git a/liteyuki/message/matcher.py b/liteyuki/message/matcher.py
new file mode 100644
index 0000000..d2add76
--- /dev/null
+++ b/liteyuki/message/matcher.py
@@ -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()
diff --git a/liteyuki/message/on.py b/liteyuki/message/on.py
new file mode 100644
index 0000000..2324662
--- /dev/null
+++ b/liteyuki/message/on.py
@@ -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)
diff --git a/liteyuki/message/rule.py b/liteyuki/message/rule.py
new file mode 100644
index 0000000..e2f3291
--- /dev/null
+++ b/liteyuki/message/rule.py
@@ -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
diff --git a/liteyuki/plugins/reloader_monitor.py b/liteyuki/message/session.py
similarity index 72%
rename from liteyuki/plugins/reloader_monitor.py
rename to liteyuki/message/session.py
index 4900e21..e125511 100644
--- a/liteyuki/plugins/reloader_monitor.py
+++ b/liteyuki/message/session.py
@@ -2,9 +2,9 @@
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
-@Time : 2024/8/10 下午5:18
+@Time : 2024/8/19 下午10:47
@Author : snowykami
@Email : snowykami@outlook.com
-@File : reloader_monitor.py
+@File : session.py
@Software: PyCharm
"""
\ No newline at end of file
diff --git a/liteyuki/mkdoc.py b/liteyuki/mkdoc.py
new file mode 100644
index 0000000..7463eca
--- /dev/null
+++ b/liteyuki/mkdoc.py
@@ -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"\n源代码
\n\n```python\n{func.source_code}\n```\n \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"\n{TEXT_SOURCE_CODE}
\n\n```python\n{method.source_code}\n```\n \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")
diff --git a/liteyuki/plugin/__init__.py b/liteyuki/plugin/__init__.py
index 7bb7edf..370b307 100644
--- a/liteyuki/plugin/__init__.py
+++ b/liteyuki/plugin/__init__.py
@@ -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
__all__ = [
- "PluginMetadata",
- "Plugin",
- "load_plugin",
- "load_plugins",
+ "PluginMetadata",
+ "Plugin",
+ "PluginType",
+ "load_plugin",
+ "load_plugins",
]
diff --git a/liteyuki/plugin/load.py b/liteyuki/plugin/load.py
index 3ef2933..97fa765 100644
--- a/liteyuki/plugin/load.py
+++ b/liteyuki/plugin/load.py
@@ -10,14 +10,12 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
"""
import os
import traceback
+from importlib import import_module
from pathlib import Path
from typing import Optional
-from nonebot import logger
-
-from liteyuki.plugin.model import Plugin, PluginMetadata
-from importlib import import_module
-
+from liteyuki.log import logger
+from liteyuki.plugin.model import Plugin, PluginMetadata, PluginType
from liteyuki.utils import path_to_module_name
_plugins: dict[str, Plugin] = {}
@@ -25,6 +23,7 @@ _plugins: dict[str, Plugin] = {}
__all__ = [
"load_plugin",
"load_plugins",
+ "_plugins",
]
@@ -35,45 +34,92 @@ def load_plugin(module_path: str | Path) -> Optional[Plugin]:
module_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:
module = import_module(module_path)
_plugins[module.__name__] = Plugin(
name=module.__name__,
module=module,
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(
- f'成功加载 轻雪插件 "{module.__name__.split(".")[-1]}"'
+ f'成功加载轻雪插件 "{display_name}"'
)
return _plugins[module.__name__]
except Exception as e:
logger.opt(colors=True).success(
- f'未能加载 轻雪插件 "{module_path}"'
+ f'无法加载轻雪插件 "{module_path}"'
)
traceback.print_exc()
return None
-def load_plugins(*plugin_dir: str) -> set[Plugin]:
+def load_plugins(*plugin_dir: str, ignore_warning: bool = True) -> set[Plugin]:
"""导入文件夹下多个插件
参数:
plugin_dir: 文件夹路径
+ ignore_warning: 是否忽略警告,通常是目录不存在或目录为空
"""
plugins = set()
for dir_path in plugin_dir:
# 遍历每一个文件夹下的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):
path = Path(os.path.join(dir_path, f))
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]}"
- 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)
if module_name:
@@ -81,3 +127,27 @@ def load_plugins(*plugin_dir: str) -> set[Plugin]:
if _plugins.get(module_name):
plugins.add(_plugins[module_name])
return plugins
+
+
+def format_display_name(display_name: str, plugin_type: PluginType) -> str:
+ """
+ 设置插件名称颜色,根据不同类型插件设置颜色
+ Args:
+ display_name: 插件名称
+ plugin_type: 插件类型
+
+ Returns:
+ str: 设置后的插件名称 name
+ """
+ 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}>"
diff --git a/liteyuki/plugin/manager.py b/liteyuki/plugin/manager.py
index ca0edbb..9107775 100644
--- a/liteyuki/plugin/manager.py
+++ b/liteyuki/plugin/manager.py
@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
"""
-Copyright (C) 2020-2024 LiteyukiStudio. All rights reserved
-
-版权所有 © 2020-2024 神羽SnowyKami & 金羿Eilles with LiteyukiStudio & TriM Org.
-保留所有权利
+Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/7/23 下午11:59
@Author : snowykami
diff --git a/liteyuki/plugin/model.py b/liteyuki/plugin/model.py
index a741415..cb066c3 100644
--- a/liteyuki/plugin/model.py
+++ b/liteyuki/plugin/model.py
@@ -8,22 +8,61 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@File : model.py
@Software: PyCharm
"""
+from enum import Enum
from types import ModuleType
-from typing import Optional
+from typing import Any, Optional
from pydantic import BaseModel
+class PluginType(Enum):
+ """
+ 插件类型枚举值
+ """
+ APPLICATION = "application"
+ """应用端:例如NoneBot"""
+
+ SERVICE = "service"
+ """服务端:例如AI绘画后端"""
+
+ MODULE = "module"
+ """模块:导出对象给其他插件使用"""
+
+ UNCLASSIFIED = "unclassified"
+ """未分类:默认值"""
+
+ TEST = "test"
+ """测试:测试插件"""
+
+
class PluginMetadata(BaseModel):
"""
轻雪插件元数据,由插件编写者提供,name为必填项
+ Attributes:
+ ----------
+
+ name: str
+ 插件名称
+ description: str
+ 插件描述
+ usage: str
+ 插件使用方法
+ type: str
+ 插件类型
+ author: str
+ 插件作者
+ homepage: str
+ 插件主页
+ extra: dict[str, Any]
+ 额外信息
"""
name: str
description: str = ""
usage: str = ""
- type: str = ""
+ type: PluginType = PluginType.UNCLASSIFIED
+ author: str = ""
homepage: str = ""
- running_in_main: bool = True # 是否在主进程运行
+ extra: dict[str, Any] = {}
class Plugin(BaseModel):
diff --git a/liteyuki/plugins/__init__.py b/liteyuki/plugins/__init__.py
new file mode 100644
index 0000000..a0fa114
--- /dev/null
+++ b/liteyuki/plugins/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+"""
+此模块为内置插件文件夹,用于存放内置插件。
+This module is the built-in plugin folder, used to store built-in plugins.
+"""
diff --git a/liteyuki/plugins/lifespan_monitor.py b/liteyuki/plugins/lifespan_monitor.py
deleted file mode 100644
index 964f2c5..0000000
--- a/liteyuki/plugins/lifespan_monitor.py
+++ /dev/null
@@ -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)
diff --git a/liteyuki/plugins/liteecho.py b/liteyuki/plugins/liteecho.py
new file mode 100644
index 0000000..1f70582
--- /dev/null
+++ b/liteyuki/plugins/liteecho.py
@@ -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())
diff --git a/liteyuki/plugins/plugin_loader/__init__.py b/liteyuki/plugins/plugin_loader/__init__.py
new file mode 100644
index 0000000..84ea543
--- /dev/null
+++ b/liteyuki/plugins/plugin_loader/__init__.py
@@ -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()
diff --git a/liteyuki/plugins/resource_loader/__init__.py b/liteyuki/plugins/resource_loader/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/liteyuki/utils.py b/liteyuki/utils.py
index 10e7ff3..629f5d7 100644
--- a/liteyuki/utils.py
+++ b/liteyuki/utils.py
@@ -4,11 +4,15 @@
"""
import asyncio
import inspect
+import multiprocessing
+import threading
from pathlib import Path
from typing import Any, Callable, Coroutine
from liteyuki.log import logger
+IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess"
+
def is_coroutine_callable(call: Callable[..., Any]) -> bool:
"""
@@ -39,7 +43,7 @@ def run_coroutine(*coro: Coroutine):
# 检测是否有现有的事件循环
try:
- loop = asyncio.get_event_loop()
+ loop = asyncio.get_running_loop()
if loop.is_running():
# 如果事件循环正在运行,创建任务
for c in coro:
@@ -59,6 +63,18 @@ def run_coroutine(*coro: Coroutine):
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:
"""
转换路径为模块名
@@ -72,3 +88,19 @@ def path_to_module_name(path: Path) -> str:
return ".".join(rel_path.parts[:-1])
else:
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
diff --git a/main.py b/main.py
index 7efe027..93bc51c 100644
--- a/main.py
+++ b/main.py
@@ -1,6 +1,10 @@
+"""
+启动脚本,会执行一些启动的操作,比如加载配置文件,初始化 bot 实例等。
+"""
from liteyuki import LiteyukiBot
-from liteyuki.config import load_from_yaml
+from liteyuki.config import load_config_in_default
+
if __name__ == "__main__":
- bot = LiteyukiBot(**load_from_yaml("config.yml"))
+ bot = LiteyukiBot(**load_config_in_default(no_waring=True))
bot.run()
diff --git a/pyproject.toml b/pyproject.toml
index e65327c..f131770 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,58 +1,44 @@
# PEP 621 project metadata
# See https://www.python.org/dev/peps/pep-0621/
-# This file is for project use, but don`t use with nb-cli
-# 此文件为项目所用,请不要和nb-cli一起使用以防被修改
-[tool.poetry]
+# This file is liteyuki framework use only, don`t use it with applications or nb-cli.
+# 此文件仅供 liteyuki 框架使用,请勿用于应用程序及nb-cli,请使用pip进行安装。
+[project]
name = "ryoun-trim"
-version = "0"
-description = "based on liteyuki6"
-authors = ["金羿Eilles"]
-license = "MIT & LSO"
-package-mode = false
+dynamic = ["version"]
+description = "A lightweight bot process management framework and application."
+readme = "README.md"
+requires-python = ">=3.10"
+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 = "汉钰律许可协议 第一版" }
-
-[tool.poetry.dependencies]
-python = "^3.10"
-aiofiles = "~23.2.1"
-aiohttp = "~3.9.3"
-aiosqlite3 = "~0.3.0"
-colored = "~2.2.4"
-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]
+dependencies = [
+ "loguru~=0.7.2",
+ "pydantic==2.8.2",
+ "PyYAML==6.0.2",
+ "toml==0.10.2",
+ "watchdog==4.0.1",
+ "pdm-backend==2.3.3"
+]
[project.urls]
-homepage = "https://bot.liteyuki.icu"
-repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
-documentation = "https://bot.liteyuki.icu"
+Homepage = "https://bot.liteyuki.icu"
+Repository = "https://gitee.com/TriM-Organization/LiteyukiBot-TriM"
+"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"
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index da4db6d..dbc9c6e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,35 +1,34 @@
-aiohttp~=3.9.3
-aiofiles~=23.2.1
-colored~=2.2.4
-GitPython~=3.1.42
-httpx~=0.27.0
-nb-cli~=1.4.1
-nonebot2[fastapi,httpx,websockets]~=2.3.0
-nonebot-plugin-htmlrender~=0.3.1
-nonebot-adapter-onebot~=2.4.3
-nonebot-plugin-alconna~=0.46.3
-nonebot_plugin_apscheduler~=0.4.0
-nonebot-adapter-satori~=0.11.5
+aiohttp>=3.9.3
+aiofiles>=23.2.1
+colored>=2.2.4
+GitPython>=3.1.43
+httpx>=0.27.0
+nonebot-plugin-htmlrender>=0.1.0
+nonebot2[fastapi,httpx,websockets]>=2.3.3
+nonebot-adapter-onebot>=2.4.3
+nonebot-plugin-alconna>=0.46.3
+nonebot_plugin_apscheduler>=0.4.0
+nonebot-adapter-satori>=0.11.5
+# pyppeteer>=2.0.0
+markdown>=3.3.6
+zhDateTime>=1.1.1
numpy<2.0.0
-packaging~=23.1
-psutil~=5.9.8
-py-cpuinfo~=9.0.0
-pydantic~=2.7.0
-Pygments~=2.17.2
-pytz~=2024.1
-PyYAML~=6.0.1
-starlette~=0.36.3
-loguru~=0.7.2
-importlib_metadata~=7.0.2
-requests~=2.31.0
-watchdog~=4.0.0
-pillow~=10.0.0
-jieba~=0.42.1
-aiosqlite3~=0.3.0
-fastapi~=0.110.0
-python-dotenv~=1.0.1
+packaging>=23.1
+psutil>=5.9.8
+py-cpuinfo>=9.0.0
+Pygments>=2.17.2
+pyppeteer>=2.0.0
+pytz>=2024.1
+PyYAML>=6.0.1
+pillow>=10.0.0
+toml>=0.10.2
+importlib_metadata>=7.0.2
+watchdog>=4.0.0
+jieba>=0.42.1
+python-dotenv>=1.0.1
nonebot_plugin_session
pypinyin
-zhDateTime>=1.0.3
Musicreater>=2.2.0
-librosa==0.10.1
\ No newline at end of file
+librosa==0.10.1
+TrimMCStruct
+brotli
\ No newline at end of file
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/liteyuki_main/__init__.py b/src/liteyuki_main/__init__.py
index 24324a0..8321e08 100644
--- a/src/liteyuki_main/__init__.py
+++ b/src/liteyuki_main/__init__.py
@@ -2,7 +2,6 @@ from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
-from .dev import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
@@ -18,29 +17,6 @@ __plugin_meta__ = PluginMetadata(
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())
nonebot.logger.info(
sys_lang.get("main.current_language", LANG=sys_lang.get("language.name"))
diff --git a/src/liteyuki_main/core.py b/src/liteyuki_main/core.py
index 8193e5f..6905ec5 100644
--- a/src/liteyuki_main/core.py
+++ b/src/liteyuki_main/core.py
@@ -1,44 +1,43 @@
-import base64
import time
-from typing import Any, AnyStr
+from typing import AnyStr
+
+import time
+from typing import AnyStr
import nonebot
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.onebot.v11 import Message, escape, unescape
-from nonebot.exception import MockApiException
+from nonebot.adapters.onebot.v11 import Message, unescape
from nonebot.internal.matcher import Matcher
from nonebot.permission import SUPERUSER
+# from src.liteyuki.core import Reloader
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.data_manager import StoredConfig, TempConfig, common_db
+from src.utils.base.config import get_config
+from src.utils.base.data_manager import TempConfig, common_db
from src.utils.base.language import get_user_lang
from src.utils.base.ly_typing import T_Bot, T_MessageEvent
from src.utils.message.message import MarkdownMessage as md, broadcast_to_superusers
-
-from .api import update_liteyuki
-from ..utils.base import reload
-from ..utils.base.ly_function import get_function
+from .api import update_liteyuki # type: ignore
+from ..utils.base import reload # type: ignore
+from ..utils.base.ly_function import get_function # type: ignore
+from ..utils.message.html_tool import md_to_pic
require("nonebot_plugin_alconna")
require("nonebot_plugin_apscheduler")
from nonebot_plugin_alconna import (
+ UniMessage,
on_alconna,
Alconna,
Args,
- Subcommand,
Arparma,
MultiVar,
)
from nonebot_plugin_apscheduler import scheduler
-driver = get_driver()
-markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).config.get(
- "markdown_image", False
-)
+driver = get_driver()
@on_alconna(
@@ -50,8 +49,8 @@ markdown_image = common_db.where_one(StoredConfig(), default=StoredConfig()).con
).handle()
# Satori OK
async def _(bot: T_Bot, matcher: Matcher, result: Arparma):
- if result.main_args.get("text"):
- await matcher.finish(Message(unescape(result.main_args.get("text")))) # type: ignore
+ if text := result.main_args.get("text"):
+ await matcher.finish(Message(unescape(text)))
else:
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
).handle()
# Satori OK
-async def _(bot: T_Bot, event: T_MessageEvent):
+async def _(bot: T_Bot, event: T_MessageEvent, matcher: Matcher):
# 使用git pull更新
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")
pip.main(["install", "-r", "requirements.txt"])
reply += f"{ulang.get('liteyuki.update_restart', RESTART=btn_restart)}"
- await md.send_md(reply, bot, event=event, at_sender=False)
+ # await md.send_md(reply, bot)
+ img_bytes = await md_to_pic(reply)
+ await UniMessage.send(UniMessage.image(raw=img_bytes))
@on_alconna(
@@ -115,108 +116,9 @@ async def _(matcher: Matcher, bot: T_Bot, event: T_MessageEvent):
)
common_db.save(temp_data)
-
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(
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}")
-# system hook
-@Bot.on_calling_api # 图片模式检测
-async def test_for_md_image(bot: T_Bot, api: str, data: dict):
- # 截获大图发送,转换为markdown发送
- if (
- api in ["send_msg", "send_private_msg", "send_group_msg"]
- and markdown_image
- and data.get("user_id") != bot.self_id
- ):
- if (
- api == "send_msg"
- and data.get("message_type") == "private"
- or api == "send_private_msg"
- ):
- session_type = "private"
- session_id = data.get("user_id")
- elif (
- api == "send_msg"
- and data.get("message_type") == "group"
- or api == "send_group_msg"
- ):
- session_type = "group"
- session_id = data.get("group_id")
- else:
- return
- if (
- len(data.get("message", [])) == 1
- and data["message"][0].get("type") == "image"
- ):
- file: str = data["message"][0].data.get("file")
- # file:// http:// base64://
- if file.startswith("http"):
- result = await md.send_md(
- await md.image_async(file),
- bot,
- message_type=session_type,
- session_id=session_id,
- )
- elif file.startswith("file"):
- file = file.replace("file://", "")
- result = await md.send_image(
- open(file, "rb").read(),
- bot,
- message_type=session_type,
- session_id=session_id,
- )
- elif file.startswith("base64"):
- file_bytes = base64.b64decode(file.replace("base64://", ""))
- result = await md.send_image(
- file_bytes, bot, message_type=session_type, session_id=session_id
- )
- else:
- return
- raise MockApiException(result=result)
-
-
@driver.on_startup
async def on_startup():
temp_data = common_db.where_one(TempConfig(), default=TempConfig())
@@ -426,6 +272,14 @@ async def _(bot: T_Bot):
group_id=reload_session_id,
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:
await bot.call_api(
"send_msg",
@@ -445,7 +299,6 @@ async def every_day_update():
if result:
await broadcast_to_superusers(f"灵温已更新:```\n{logs}\n```")
nonebot.logger.info(f"灵温已更新:{logs}")
- # ProcessingManager.restart(3)
reload()
else:
nonebot.logger.info(logs)
diff --git a/src/liteyuki_main/dev.py b/src/liteyuki_main/dev.py
deleted file mode 100644
index 49f90ca..0000000
--- a/src/liteyuki_main/dev.py
+++ /dev/null
@@ -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()
diff --git a/src/liteyuki_main/loader.py b/src/liteyuki_main/loader.py
index 199c9d3..91df45e 100644
--- a/src/liteyuki_main/loader.py
+++ b/src/liteyuki_main/loader.py
@@ -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.message.tools import check_for_package
-from liteyuki import get_bot, chan
-
-from nonebot_plugin_apscheduler import scheduler
-
load_resources()
init_log()
driver = get_driver()
-liteyuki_bot = get_bot()
@driver.on_startup
@@ -32,7 +27,7 @@ async def load_plugins():
for installed_plugin in installed_plugins:
if not check_for_package(installed_plugin.module_name):
nonebot.logger.error(
- f"插件 {installed_plugin.module_name} 在加载列表中但未安装。请使用超管账户对机器人发送 `npm fixup` 以重新安装。"
+ f"插件 {installed_plugin.module_name} 仍在加载列表中但未安装。"
)
else:
nonebot.load_plugin(installed_plugin.module_name)
diff --git a/src/liteyuki_plugins/README.md b/src/liteyuki_plugins/README.md
deleted file mode 100644
index aba0376..0000000
--- a/src/liteyuki_plugins/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# 说明
-
-此目录为**轻雪插件**目录,非其他插件目录。
\ No newline at end of file
diff --git a/src/liteyuki_plugins/hello_liteyuki.py b/src/liteyuki_plugins/hello_liteyuki.py
new file mode 100644
index 0000000..86314c3
--- /dev/null
+++ b/src/liteyuki_plugins/hello_liteyuki.py
@@ -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("你好呀")
diff --git a/src/liteyuki_plugins/lifespan_monitor.py b/src/liteyuki_plugins/lifespan_monitor.py
new file mode 100644
index 0000000..68f3b81
--- /dev/null
+++ b/src/liteyuki_plugins/lifespan_monitor.py
@@ -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("生命周期监控器:启动完成")
diff --git a/src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py
new file mode 100644
index 0000000..a0c6add
--- /dev/null
+++ b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/__init__.py
@@ -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)
diff --git a/src/liteyuki_plugins/liteyukibot_plugin_nonebot/dev_reloader.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/dev_reloader.py
new file mode 100644
index 0000000..8bebd08
--- /dev/null
+++ b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/dev_reloader.py
@@ -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")
\ No newline at end of file
diff --git a/liteyuki/core/nb/adapter_manager/__init__.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/__init__.py
similarity index 100%
rename from liteyuki/core/nb/adapter_manager/__init__.py
rename to src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/__init__.py
diff --git a/liteyuki/core/nb/adapter_manager/onebot.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/onebot.py
similarity index 100%
rename from liteyuki/core/nb/adapter_manager/onebot.py
rename to src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/onebot.py
diff --git a/liteyuki/core/nb/adapter_manager/satori.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/satori.py
similarity index 71%
rename from liteyuki/core/nb/adapter_manager/satori.py
rename to src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/satori.py
index 0dfa23d..44edb8b 100644
--- a/liteyuki/core/nb/adapter_manager/satori.py
+++ b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/adapter_manager/satori.py
@@ -7,14 +7,14 @@ from nonebot.adapters import satori
def init(config: dict):
if config.get("satori", None) is None:
- nonebot.logger.info("未查见 Satori 的配置文档,将跳过 Satori 初始化")
+ nonebot.logger.info("未寻得 Satori 设定信息,跳过初始化")
return None
satori_config = config.get("satori")
if not satori_config.get("enable", False):
- nonebot.logger.info("未启用 Satori ,将跳过 Satori 初始化")
+ nonebot.logger.info("Satori 未启用,跳过初始化")
return 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)
config['satori_clients'] = satori_config.get("hosts", [])
return
diff --git a/liteyuki/core/nb/driver_manager/__init__.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/__init__.py
similarity index 100%
rename from liteyuki/core/nb/driver_manager/__init__.py
rename to src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/__init__.py
diff --git a/liteyuki/core/nb/driver_manager/auto_set_env.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/auto_set_env.py
similarity index 67%
rename from liteyuki/core/nb/driver_manager/auto_set_env.py
rename to src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/auto_set_env.py
index b79565e..982c988 100644
--- a/liteyuki/core/nb/driver_manager/auto_set_env.py
+++ b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/auto_set_env.py
@@ -9,12 +9,12 @@ from .defines import *
def auto_set_env(config: dict):
dotenv.load_dotenv(".env")
if os.getenv("DRIVER", None) is not None:
- nonebot.logger.info("Driver 已设入环境变量中,将跳过自动配置环节。")
+ nonebot.logger.info("Driver 已在环境变量中配置,跳过自动设定")
return
if config.get("satori", {'enable': False}).get("enable", False):
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:
os.environ["DRIVER"] = get_driver_string(ASGI_DRIVER)
- nonebot.logger.info("已禁用 Satori,将 driver 设为 ASGI")
+ nonebot.logger.info("禁用 Satori,已设定 Driver 为 ASGI")
return
diff --git a/liteyuki/core/nb/driver_manager/defines.py b/src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/defines.py
similarity index 100%
rename from liteyuki/core/nb/driver_manager/defines.py
rename to src/liteyuki_plugins/liteyukibot_plugin_nonebot/nb_utils/driver_manager/defines.py
diff --git a/src/liteyuki_plugins/process_manager/__init__.py b/src/liteyuki_plugins/process_manager/__init__.py
new file mode 100644
index 0000000..8c4be52
--- /dev/null
+++ b/src/liteyuki_plugins/process_manager/__init__.py
@@ -0,0 +1,8 @@
+from liteyuki.plugin import PluginMetadata, PluginType
+
+__plugin_meta__ = PluginMetadata(
+ name="进程管理器",
+ author="snowykami",
+ description="进程管理器,用于管理子进程",
+ type=PluginType.SERVICE
+)
diff --git a/liteyuki/plugins/register_service.py b/src/liteyuki_plugins/register_service.py
similarity index 50%
rename from liteyuki/plugins/register_service.py
rename to src/liteyuki_plugins/register_service.py
index 200bd15..2fd4c6d 100644
--- a/liteyuki/plugins/register_service.py
+++ b/src/liteyuki_plugins/register_service.py
@@ -11,21 +11,22 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
import json
import os.path
import platform
-
-import requests
+from aiohttp import ClientSession
from git import Repo
-from liteyuki.plugin import PluginMetadata
+
+from liteyuki.plugin import PluginMetadata, PluginType
from liteyuki import get_bot, logger
__plugin_meta__ = PluginMetadata(
name="注册服务",
+ type=PluginType.SERVICE
)
liteyuki = get_bot()
commit_hash = Repo(".").head.commit.hexsha
-def register_bot():
+async def register_bot():
url = "https://api.liteyuki.icu/register"
data = {
"name" : "尹灵温|轻雪-睿乐",
@@ -36,17 +37,19 @@ def register_bot():
"os" : f"{platform.system()} {platform.version()} {platform.machine()}"
}
try:
- 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"))
- logger.success("成功将 {} 注册到 Liteyuki 服务器".format(liteyuki_id))
- else:
- raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
-
+ logger.info("正在等待 Liteyuki 注册服务器…")
+ async with ClientSession() as session:
+ async with session.post(url, json=data, timeout=15) as resp:
+ if resp.status == 200:
+ data = await 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"))
+ logger.success("成功将 {} 注册到 Liteyuki 服务器".format(liteyuki_id))
+ else:
+ raise ValueError(f"无法向 Liteyuki 服务器注册:{data}")
+ else:
+ raise ValueError(f"无法向 Liteyuki 服务器注册:{resp.status}")
except Exception as e:
logger.warning(f"虽然向 Liteyuki 服务器注册失败,但无关紧要:{e}")
@@ -54,4 +57,6 @@ def register_bot():
@liteyuki.on_before_start
async def _():
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()
diff --git a/src/liteyuki_plugins/resource_loader/__init__.py b/src/liteyuki_plugins/resource_loader/__init__.py
new file mode 100644
index 0000000..9eaecf7
--- /dev/null
+++ b/src/liteyuki_plugins/resource_loader/__init__.py
@@ -0,0 +1,8 @@
+from liteyuki.plugin import PluginMetadata, PluginType
+
+__plugin_meta__ = PluginMetadata(
+ name="资源加载器",
+ author="snowykami",
+ description="进程管理器,用于管理子进程",
+ type=PluginType.SERVICE
+)
diff --git a/src/liteyuki_plugins/scheduled_tasks/__init__.py b/src/liteyuki_plugins/scheduled_tasks/__init__.py
new file mode 100644
index 0000000..03cffe5
--- /dev/null
+++ b/src/liteyuki_plugins/scheduled_tasks/__init__.py
@@ -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
+)
diff --git a/src/liteyuki_plugins/scheduled_tasks/divided_by_lifespan/__init__.py b/src/liteyuki_plugins/scheduled_tasks/divided_by_lifespan/__init__.py
new file mode 100644
index 0000000..a74b17b
--- /dev/null
+++ b/src/liteyuki_plugins/scheduled_tasks/divided_by_lifespan/__init__.py
@@ -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 *
\ No newline at end of file
diff --git a/src/liteyuki_plugins/scheduled_tasks/divided_by_lifespan/after_start.py b/src/liteyuki_plugins/scheduled_tasks/divided_by_lifespan/after_start.py
new file mode 100644
index 0000000..bd3cc2a
--- /dev/null
+++ b/src/liteyuki_plugins/scheduled_tasks/divided_by_lifespan/after_start.py
@@ -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)
diff --git a/src/nonebot_plugins/liteyuki_crt_utils/__init__.py b/src/nonebot_plugins/liteyuki_crt_utils/__init__.py
deleted file mode 100644
index 386f3c6..0000000
--- a/src/nonebot_plugins/liteyuki_crt_utils/__init__.py
+++ /dev/null
@@ -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子进程已接收到数据")
diff --git a/src/nonebot_plugins/liteyuki_crt_utils/canvas.py b/src/nonebot_plugins/liteyuki_crt_utils/canvas.py
deleted file mode 100644
index 02e6e3d..0000000
--- a/src/nonebot_plugins/liteyuki_crt_utils/canvas.py
+++ /dev/null
@@ -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")
diff --git a/src/nonebot_plugins/liteyuki_crt_utils/crt.py b/src/nonebot_plugins/liteyuki_crt_utils/crt.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py b/src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py
deleted file mode 100644
index 0a4fa64..0000000
--- a/src/nonebot_plugins/liteyuki_crt_utils/crt_matchers.py
+++ /dev/null
@@ -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))
diff --git a/src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py b/src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py
deleted file mode 100644
index 4167cbf..0000000
--- a/src/nonebot_plugins/liteyuki_crt_utils/rt_guide.py
+++ /dev/null
@@ -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号线&Line1ff0000,27号线&Line1ff0000 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()
diff --git a/src/nonebot_plugins/liteyuki_minigame/__init__.py b/src/nonebot_plugins/liteyuki_minigame/__init__.py
deleted file mode 100644
index 3dd85d9..0000000
--- a/src/nonebot_plugins/liteyuki_minigame/__init__.py
+++ /dev/null
@@ -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,
- }
-)
diff --git a/src/nonebot_plugins/liteyuki_minigame/game.py b/src/nonebot_plugins/liteyuki_minigame/game.py
deleted file mode 100644
index 8ed838c..0000000
--- a/src/nonebot_plugins/liteyuki_minigame/game.py
+++ /dev/null
@@ -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
diff --git a/src/nonebot_plugins/liteyuki_minigame/minesweeper.py b/src/nonebot_plugins/liteyuki_minigame/minesweeper.py
deleted file mode 100644
index 715bb7a..0000000
--- a/src/nonebot_plugins/liteyuki_minigame/minesweeper.py
+++ /dev/null
@@ -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("参数错误")
diff --git a/src/nonebot_plugins/liteyuki_pacman/npm.py b/src/nonebot_plugins/liteyuki_pacman/npm.py
index 97cb3d3..35f54a1 100644
--- a/src/nonebot_plugins/liteyuki_pacman/npm.py
+++ b/src/nonebot_plugins/liteyuki_pacman/npm.py
@@ -14,17 +14,21 @@ from nonebot.permission import SUPERUSER
from nonebot.plugin import Plugin, PluginMetadata
from nonebot.utils import run_sync
+
from src.utils.base.data_manager import InstalledPlugin
from src.utils.base.language import get_user_lang
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.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 *
+
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import (
+ UniMessage,
on_alconna,
Alconna,
Args,
@@ -147,7 +151,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
session_id = event.group_id
new_event = event
else:
- raise FinishedException(ulang.get("Permission Denied"))
+ raise FinishedException(ulang.get("liteyuki.permission_denied"))
session_enable = get_plugin_session_enable(
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)}"
else:
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:
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(
ulang.get("npm.install_success", NAME=store_plugin.name)
) # markdown转义
- await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
+ await npm.send(f"{info}\n\n" + f"\n{log}\n")
else:
await npm.finish(
ulang.get(
@@ -331,12 +336,12 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
info = ulang.get(
"npm.load_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
).replace("_", r"\\_")
- await md.send_md(f"{info}\n\n" f"```\n{log}\n```\n", bot, event=event)
+ await npm.finish(f"{info}\n\n" f"```\n{log}\n```\n")
else:
info = ulang.get(
"npm.install_failed", NAME=plugin_name, HOMEPAGE=homepage_btn
).replace("_", r"\\_")
- await md.send_md(f"{info}\n\n" f"```\n{log}\n```", bot, event=event)
+ await npm.send(f"{info}\n\n" f"```\n{log}\n```")
elif sc.get("uninstall") and perm_s:
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")
)
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:
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>*{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:
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>*{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(
@@ -554,7 +562,7 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot, npm: Matcher):
Subcommand(
disable,
Args["group_id", str, None],
- alias=["d", "停用", "禁用"],
+ alias=["d", "停用"],
),
),
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"))
),
]
- await md.send_md(compile_md(reply), bot, event=event)
+ await matcher.finish(compile_md(reply))
else:
await matcher.finish(ulang.get("npm.plugin_not_found", NAME=plugin_name))
else:
diff --git a/src/nonebot_plugins/liteyuki_pacman/rpm.py b/src/nonebot_plugins/liteyuki_pacman/rpm.py
index 732372e..9f8def1 100644
--- a/src/nonebot_plugins/liteyuki_pacman/rpm.py
+++ b/src/nonebot_plugins/liteyuki_pacman/rpm.py
@@ -198,7 +198,7 @@ async def _(bot: T_Bot, event: T_MessageEvent, result: Arparma, matcher: Matcher
else:
pass
if send_as_md:
- await md.send_md(reply, bot, event=event)
+ await matcher.send(reply)
else:
if reply:
await matcher.finish(reply)
diff --git a/src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py b/src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py
index 64fc573..9533b05 100644
--- a/src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py
+++ b/src/nonebot_plugins/liteyuki_satori_user_info/auto_update.py
@@ -2,7 +2,6 @@ import nonebot
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 import satori_utils
from nonebot.adapters import satori
diff --git a/src/nonebot_plugins/liteyuki_sign_status.py b/src/nonebot_plugins/liteyuki_sign_status.py
deleted file mode 100644
index 5f3ad44..0000000
--- a/src/nonebot_plugins/liteyuki_sign_status.py
+++ /dev/null
@@ -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
diff --git a/src/nonebot_plugins/liteyuki_statistics/data_source.py b/src/nonebot_plugins/liteyuki_statistics/data_source.py
index 64b67cf..e39d7bc 100644
--- a/src/nonebot_plugins/liteyuki_statistics/data_source.py
+++ b/src/nonebot_plugins/liteyuki_statistics/data_source.py
@@ -61,6 +61,8 @@ async def get_stat_msg_image(
condition_args.append(user_id)
msg_rows = msg_db.where_all(MessageEventModel(), condition, *condition_args)
+ if not msg_rows:
+ msg_rows = []
timestamps = []
msg_count = []
msg_rows.sort(key=lambda x: x.time)
@@ -157,8 +159,8 @@ async def get_stat_rank_image(
templates = {
"data": {
"name": ulang.get("stat.rank")
- + f" 类别:{rank_type}"
- + f" 制约:{limit}",
+ + f" Type {rank_type}"
+ + f" Limit {limit}",
"ranking": ranking,
}
}
diff --git a/src/nonebot_plugins/liteyuki_statistics/stat_matchers.py b/src/nonebot_plugins/liteyuki_statistics/stat_matchers.py
index 2c322f1..d631c4f 100644
--- a/src/nonebot_plugins/liteyuki_statistics/stat_matchers.py
+++ b/src/nonebot_plugins/liteyuki_statistics/stat_matchers.py
@@ -96,8 +96,10 @@ async def _(result: Arparma, event: T_MessageEvent, bot: Bot):
bot_id = result.other_args.get("bot_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))
+ else:
+ group_id = "all"
if group_id in ["all", "a"]:
group_id = "all"
diff --git a/src/nonebot_plugins/liteyuki_status/api.py b/src/nonebot_plugins/liteyuki_status/api.py
index 4145dcf..c432dd9 100644
--- a/src/nonebot_plugins/liteyuki_status/api.py
+++ b/src/nonebot_plugins/liteyuki_status/api.py
@@ -7,7 +7,8 @@ from cpuinfo import cpuinfo
from nonebot import require
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.data_manager import TempConfig, common_db
from src.utils.base.language import Language
@@ -227,11 +228,19 @@ async def get_hardware_data() -> dict:
pass
swap = psutil.swap_memory()
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"
- elif "Intel" in cpu_brand_raw:
+ elif "intel" in cpu_brand_raw:
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 = "英伟达"
else:
brand = "未知处理器"
@@ -262,7 +271,9 @@ async def get_hardware_data() -> dict:
for disk in psutil.disk_partitions(all=True):
try:
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 # 虚拟磁盘
result["disk"].append(
{
@@ -283,7 +294,7 @@ async def get_liteyuki_data() -> dict:
temp_data: TempConfig = common_db.where_one(TempConfig(), default=TempConfig())
result = {
"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()),
"resources": len(get_loaded_resource_packs()),
"nonebot": f"{nonebot.__version__}",
diff --git a/src/nonebot_plugins/liteyuki_uniblacklist/api.py b/src/nonebot_plugins/liteyuki_uniblacklist/api.py
index 52ca276..0e63184 100644
--- a/src/nonebot_plugins/liteyuki_uniblacklist/api.py
+++ b/src/nonebot_plugins/liteyuki_uniblacklist/api.py
@@ -1,7 +1,6 @@
import datetime
import aiohttp
-import httpx
import nonebot
from nonebot import require
from nonebot.exception import IgnoredException
diff --git a/src/nonebot_plugins/liteyuki_user/profile_manager.py b/src/nonebot_plugins/liteyuki_user/profile_manager.py
index bdde211..b14cced 100644
--- a/src/nonebot_plugins/liteyuki_user/profile_manager.py
+++ b/src/nonebot_plugins/liteyuki_user/profile_manager.py
@@ -5,15 +5,24 @@ from nonebot import require
from src.utils.base.data import LiteModel, Database
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.message.message import MarkdownMessage as md
+
+# from src.utils.message.html_tool import md_to_pic
from .const import representative_timezones_list
from src.utils import event as event_utils
+
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna
+
profile_alc = on_alconna(
Alconna(
"profile",
@@ -28,7 +37,7 @@ profile_alc = on_alconna(
alias=["g", "查询"],
),
),
- aliases={"用户信息"}
+ aliases={"用户信息"},
)
@@ -42,13 +51,21 @@ class Profile(LiteModel):
@profile_alc.handle()
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),
- default=User(user_id=str(event_utils.get_user_id(event))))
+ user: User = user_db.where_one(
+ 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)))
if result.subcommands.get("set"):
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:
user.profile[result.args["key"]] = result.args["value"]
user_db.save(user) # 数据库保存
@@ -56,18 +73,28 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
ulang.get(
"user.profile.set_success",
ATTR=ulang.get(f"user.profile.{result.args['key']}"),
- VALUE=result.args["value"]
+ VALUE=result.args["value"],
)
)
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:
# 未输入值,尝试呼出菜单
menu = get_profile_menu(result.args["key"], ulang)
if menu:
await md.send_md(menu, bot, event=event)
else:
- await profile_alc.finish(ulang.get("user.profile.input_value", ATTR=ulang.get(f"user.profile.{result.args['key']}")))
+ 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"]
@@ -92,11 +119,16 @@ async def _(result: Arparma, event: T_MessageEvent, bot: T_Bot):
continue
val = profile.dict()[key]
key_text = ulang.get(f"user.profile.{key}")
- btn_set = md.btn_cmd(ulang.get("user.profile.edit"), f"profile set {key}",
- enter=True if key in enter_attr else False)
- reply += (f"\n**{key_text}** **{val}**\n"
- f"\n> {ulang.get(f'user.profile.{key}.desc')}"
- f"\n> {btn_set} \n\n***\n")
+ btn_set = md.btn_cmd(
+ ulang.get("user.profile.edit"),
+ f"profile set {key}",
+ enter=True if key in enter_attr else False,
+ )
+ reply += (
+ f"\n**{key_text}** **{val}**\n"
+ f"\n> {ulang.get(f'user.profile.{key}.desc')}"
+ f"\n> {btn_set} \n\n***\n"
+ )
await md.send_md(reply, bot, event=event)
@@ -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"
if key == "lang":
for lang_code, lang_name in get_all_lang().items():
- btn_set_lang = md.btn_cmd(f"{lang_name}({lang_code})", f"profile set {key} {lang_code}")
+ btn_set_lang = md.btn_cmd(
+ f"{lang_name}({lang_code})", f"profile set {key} {lang_code}"
+ )
reply += f"\n{btn_set_lang}\n***\n"
elif key == "timezone":
for tz in representative_timezones_list:
diff --git a/liteyuki/plugins/process_manager/__init__.py b/src/nonebot_plugins/pasyaut_plugin_upskin/__init__.py
similarity index 100%
rename from liteyuki/plugins/process_manager/__init__.py
rename to src/nonebot_plugins/pasyaut_plugin_upskin/__init__.py
diff --git a/src/nonebot_plugins/to_liteyuki.py b/src/nonebot_plugins/to_liteyuki.py
new file mode 100644
index 0000000..706d62e
--- /dev/null
+++ b/src/nonebot_plugins/to_liteyuki.py
@@ -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"])
diff --git a/src/resources/lagrange_sign/metadata.yml b/src/resources/lagrange_sign/metadata.yml
deleted file mode 100644
index bbf05c9..0000000
--- a/src/resources/lagrange_sign/metadata.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-name: Sign Status
-description: for Lagrange
-version: 2024.4.26
\ No newline at end of file
diff --git a/src/resources/lagrange_sign/templates/css/sign_status.css b/src/resources/lagrange_sign/templates/css/sign_status.css
deleted file mode 100644
index af18499..0000000
--- a/src/resources/lagrange_sign/templates/css/sign_status.css
+++ /dev/null
@@ -1,4 +0,0 @@
-.sign-chart {
- height: 400px;
- background-color: rgba(255, 255, 255, 0.7);
-}
\ No newline at end of file
diff --git a/src/resources/lagrange_sign/templates/js/sign_status.js b/src/resources/lagrange_sign/templates/js/sign_status.js
deleted file mode 100644
index b8deaf1..0000000
--- a/src/resources/lagrange_sign/templates/js/sign_status.js
+++ /dev/null
@@ -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
-}
\ No newline at end of file
diff --git a/src/resources/lagrange_sign/templates/sign_status.html b/src/resources/lagrange_sign/templates/sign_status.html
deleted file mode 100644
index b9eafce..0000000
--- a/src/resources/lagrange_sign/templates/sign_status.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
- Liteyuki Status
-
-
-
-
-
-
-
-
-
-
-
-{{ data | tojson }}
-
-
-
-
-
\ No newline at end of file
diff --git a/src/resources/vanilla_language/lang/zh-WY.lang b/src/resources/vanilla_language/lang/zh-WY.lang
index 2379d98..772c5ac 100644
--- a/src/resources/vanilla_language/lang/zh-WY.lang
+++ b/src/resources/vanilla_language/lang/zh-WY.lang
@@ -1,10 +1,10 @@
language.name=文言
-log.debug=试言
-log.info=讯文
-log.warning=警示
-log.error=查误
-log.success=名成
+log.debug=试
+log.info=讯
+log.warning=警
+log.error=误
+log.success=成
liteyuki.restart=复启
liteyuki.restart_now=即复启
diff --git a/src/resources/vanilla_resource/templates/css/status.css b/src/resources/vanilla_resource/templates/css/status.css
index 779ea84..8f177a9 100644
--- a/src/resources/vanilla_resource/templates/css/status.css
+++ b/src/resources/vanilla_resource/templates/css/status.css
@@ -113,6 +113,7 @@
z-index: 1;
}
+/*
.disk-title {
position: absolute;
color: var(--main-text-color);
@@ -126,6 +127,31 @@
max-width: calc(100% - 40px);
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 {
font-size: 42px;
diff --git a/src/resources/vanilla_resource/templates/js/status.js b/src/resources/vanilla_resource/templates/js/status.js
index e6cfee4..f6df8fe 100644
--- a/src/resources/vanilla_resource/templates/js/status.js
+++ b/src/resources/vanilla_resource/templates/js/status.js
@@ -1,9 +1,9 @@
-const data = JSON.parse(document.getElementById('data').innerText);
-const bot_data = data['bot']; // 机器人数据
-const hardwareData = data['hardware']; // 硬件数据
-const liteyukiData = data['liteyuki']; // LiteYuki数据
-const localData = data['localization']; // 本地化语言数据
-const motto_ = data['motto']; // 言论数据
+const data = JSON.parse(document.getElementById("data").innerText);
+const bot_data = data["bot"]; // 机器人数据
+const hardwareData = data["hardware"]; // 硬件数据
+const liteyukiData = data["liteyuki"]; // LiteYuki数据
+const localData = data["localization"]; // 本地化语言数据
+const motto_ = data["motto"]; // 言论数据
/**
* 创建CPU/内存/交换饼图
@@ -16,52 +16,54 @@ function createPieChartOption(title, data) {
animation: false,
title: {
text: title,
- left: 'center',
- top: 'center',
+ left: "center",
+ top: "center",
textStyle: {
- color: '#000',
+ color: "#000",
fontSize: 30,
- lineHeight: 36
- }
+ lineHeight: 36,
+ },
},
tooltip: {
show: true,
- trigger: 'item',
- backgroundColor: '#000',
+ trigger: "item",
+ backgroundColor: "#000",
},
- color: data.length === 3 ? ['#053349', '#007ebd', "#00000044"] : ['#007ebd', '#00000044'],
+ color:
+ data.length === 3
+ ? ["#053349", "#007ebd", "#00000044"]
+ : ["#007ebd", "#00000044"],
series: [
{
- name: 'info',
- type: 'pie',
- radius: ['80%', '100%'],
- center: ['50%', '50%'],
+ name: "info",
+ type: "pie",
+ radius: ["80%", "100%"],
+ center: ["50%", "50%"],
itemStyle: {
normal: {
label: {
- show: false
+ show: false,
},
labelLine: {
- show: false
- }
+ show: false,
+ },
},
emphasis: {
label: {
show: true,
textStyle: {
- fontSize: '50',
- fontWeight: 'bold'
- }
- }
- }
+ fontSize: "50",
+ fontWeight: "bold",
+ },
+ },
+ },
},
- data: data
- }
- ]
- }
+ data: data,
+ },
+ ],
+ };
}
-
function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
let isNegative = size < 0;
size = Math.abs(size);
@@ -81,7 +83,7 @@ function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
}
if (addUnit) {
- return size.toFixed(precision) + suffix.replace('X', unit);
+ return size.toFixed(precision) + suffix.replace("X", unit);
} else {
return size;
}
@@ -92,206 +94,260 @@ function convertSize(size, precision = 2, addUnit = true, suffix = " X字节") {
* @param title
* @param percent 数据
*/
-function createBarChart(title, percent) {
+function createBarChart(title, percent, name) {
// percent为百分比,最大值为100
- let diskDiv = document.createElement('div')
- diskDiv.setAttribute('class', 'disk-info')
- diskDiv.style.marginBottom = '20px'
+ let diskDiv = document.createElement("div");
+ diskDiv.setAttribute("class", "disk-info");
+ diskDiv.style.marginBottom = "20px";
diskDiv.innerHTML = `
${title}
- `
+ `;
+ 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) {
- let days = Math.floor(seconds / 86400)
- let hours = Math.floor((seconds % 86400) / 3600)
- let minutes = Math.floor((seconds % 3600) / 60)
- let seconds_ = Math.floor(seconds % 60)
- return `${days}${localData['days']} ${hours}${localData['hours']} ${minutes}${localData['minutes']} ${seconds_}${localData['seconds']}`
+ let days = Math.floor(seconds / 86400);
+ let hours = Math.floor((seconds % 86400) / 3600);
+ let minutes = Math.floor((seconds % 3600) / 60);
+ let seconds_ = Math.floor(seconds % 60);
+ return `${days}${localData["days"]} ${hours}${localData["hours"]} ${minutes}${localData["minutes"]} ${seconds_}${localData["seconds"]}`;
}
// 主函数
function main() {
// 添加机器人信息
- bot_data['bots'].forEach(
- (bot) => {
- let botInfoDiv = document.importNode(document.getElementById('bot-template').content, true) // 复制模板
+ bot_data["bots"].forEach((bot) => {
+ let botInfoDiv = document.importNode(
+ document.getElementById("bot-template").content,
+ true
+ ); // 复制模板
+ // 设置机器人信息
+ botInfoDiv.className = "info-box bot-info";
- // 设置机器人信息
- botInfoDiv.className = 'info-box bot-info'
-
- botInfoDiv.querySelector('.bot-icon-img').setAttribute('src', bot['icon'])
- botInfoDiv.querySelector('.bot-name').innerText = bot['name']
- let tagArray = [
- bot['protocol_name'],
- `${bot['app_name']}`,
- `${localData['groups']}${bot['groups']}`,
- `${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'
- tagSpan.innerText = tag
- // 给最后一个标签不添加后缀
- tagSpan.setAttribute('suffix', (index === 0) || (tag[0] == '\n') ? '0' : '1')
- botInfoDiv.querySelector('.bot-tags').appendChild(tagSpan)
- }
- )
- document.body.insertBefore(botInfoDiv, document.getElementById('hardware-info')) // 插入对象
-
- }
- )
+ botInfoDiv.querySelector(".bot-icon-img").setAttribute("src", bot["icon"]);
+ botInfoDiv.querySelector(".bot-name").innerText = bot["name"];
+ let tagArray = [
+ bot["protocol_name"],
+ `${bot["app_name"]}`,
+ `${localData["groups"]}${bot["groups"]}`,
+ `${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";
+ tagSpan.innerText = tag;
+ // 给最后一个标签不添加后缀
+ tagSpan.setAttribute("suffix", index === 0 || tag[0] == "\n" ? "0" : "1");
+ botInfoDiv.querySelector(".bot-tags").appendChild(tagSpan);
+ });
+ document.body.insertBefore(
+ botInfoDiv,
+ document.getElementById("hardware-info")
+ ); // 插入对象
+ });
// 添加轻雪信息
- let liteyukiInfoDiv = document.importNode(document.getElementById('bot-template').content, true) // 复制模板
- liteyukiInfoDiv.className = 'info-box bot-info'
- liteyukiInfoDiv.querySelector('.bot-icon-img').setAttribute('src', './img/litetrimo.png')
- liteyukiInfoDiv.querySelector('.bot-name').innerText = `${liteyukiData['name']} - 睿乐`
+ let liteyukiInfoDiv = document.importNode(
+ document.getElementById("bot-template").content,
+ true
+ ); // 复制模板
+ 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 = [
- `灵温 ${liteyukiData['version']}`,
- `Nonebot ${liteyukiData['nonebot']}`,
- `${liteyukiData['python']}`,
- liteyukiData['system'],
- `${localData['plugins']}${liteyukiData['plugins']}`,
- `${localData['resources']}${liteyukiData['resources']}`,
- `${localData['bots']}${liteyukiData['bots']}`,
- `${localData['runtime']} ${secondsToTextTime(liteyukiData['runtime'])}`,
- ]
- tagArray.forEach(
- (tag, index) => {
- let tagSpan = document.createElement('span')
- tagSpan.className = 'bot-tag'
- tagSpan.innerText = tag
- // 给最后一个标签不添加后缀
- tagSpan.setAttribute('suffix', (index === 0) || (tag[0] == '\n') ? '0' : '1')
- liteyukiInfoDiv.querySelector('.bot-tags').appendChild(tagSpan)
- }
- )
- document.body.insertBefore(liteyukiInfoDiv, document.getElementById('hardware-info')) // 插入对象
+ `灵温 ${liteyukiData["version"]}`,
+ `Nonebot ${liteyukiData["nonebot"]}`,
+ `${liteyukiData["python"]}`,
+ liteyukiData["system"],
+ `${localData["plugins"]}${liteyukiData["plugins"]}`,
+ `${localData["resources"]}${liteyukiData["resources"]}`,
+ `${localData["bots"]}${liteyukiData["bots"]}`,
+ `${localData["runtime"]} ${secondsToTextTime(liteyukiData["runtime"])}`,
+ ];
+ tagArray.forEach((tag, index) => {
+ let tagSpan = document.createElement("span");
+ tagSpan.className = "bot-tag";
+ tagSpan.innerText = tag;
+ // 给最后一个标签不添加后缀
+ tagSpan.setAttribute("suffix", index === 0 || tag[0] == "\n" ? "0" : "1");
+ liteyukiInfoDiv.querySelector(".bot-tags").appendChild(tagSpan);
+ });
+ document.body.insertBefore(
+ liteyukiInfoDiv,
+ document.getElementById("hardware-info")
+ ); // 插入对象
// 添加硬件信息
- const cpuData = hardwareData['cpu']
- const memData = hardwareData['memory']
- const swapData = hardwareData['swap']
+ const cpuData = hardwareData["cpu"];
+ const memData = hardwareData["memory"];
+ const swapData = hardwareData["swap"];
const cpuTagArray = [
- cpuData['name'],
- `${cpuData['cores']}${localData['cores']} ${cpuData['threads']}${localData['threads']}`,
- `${(cpuData['freq'] / 1000).toFixed(2)}吉赫兹`
- ]
+ cpuData["name"],
+ `${cpuData["cores"]}${localData["cores"]} ${cpuData["threads"]}${localData["threads"]}`,
+ `${(cpuData["freq"] / 1000).toFixed(2)}吉赫兹`,
+ ];
const memTagArray = [
- `${localData['process']} ${convertSize(memData['usedProcess'])}`,
- `${localData['used']} ${convertSize(memData['used'])}`,
- `${localData['free']} ${convertSize(memData['free'])}`,
- `${localData['total']} ${convertSize(memData['total'])}`
- ]
+ `${localData["process"]} ${convertSize(memData["usedProcess"])}`,
+ `${localData["used"]} ${convertSize(memData["used"])}`,
+ `${localData["free"]} ${convertSize(memData["free"])}`,
+ `${localData["total"]} ${convertSize(memData["total"])}`,
+ ];
const swapTagArray = [
- `${localData['used']} ${convertSize(swapData['used'])}`,
- `${localData['free']} ${convertSize(swapData['free'])}`,
- `${localData['total']} ${convertSize(swapData['total'])}`
- ]
- let cpuDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
- let memDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
- let swapDeviceInfoDiv = document.importNode(document.getElementById('device-info').content, true)
+ `${localData["used"]} ${convertSize(swapData["used"])}`,
+ `${localData["free"]} ${convertSize(swapData["free"])}`,
+ `${localData["total"]} ${convertSize(swapData["total"])}`,
+ ];
+ let cpuDeviceInfoDiv = document.importNode(
+ document.getElementById("device-info").content,
+ 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')
- memDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'mem-info')
- swapDeviceInfoDiv.querySelector('.device-info').setAttribute('id', 'swap-info')
- cpuDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'cpu-chart')
- memDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'mem-chart')
- swapDeviceInfoDiv.querySelector('.device-chart').setAttribute('id', 'swap-chart')
+ cpuDeviceInfoDiv.querySelector(".device-info").setAttribute("id", "cpu-info");
+ memDeviceInfoDiv.querySelector(".device-info").setAttribute("id", "mem-info");
+ swapDeviceInfoDiv
+ .querySelector(".device-info")
+ .setAttribute("id", "swap-info");
+ 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 = {
- 'cpu': cpuDeviceInfoDiv,
- 'mem': memDeviceInfoDiv,
- 'swap': swapDeviceInfoDiv
- }
+ cpu: cpuDeviceInfoDiv,
+ mem: memDeviceInfoDiv,
+ swap: swapDeviceInfoDiv,
+ };
// 遍历添加标签
for (let device in devices) {
- let tagArray = []
+ let tagArray = [];
switch (device) {
- case 'cpu':
- tagArray = cpuTagArray
- break
- case 'mem':
- tagArray = memTagArray
- break
- case 'swap':
- tagArray = swapTagArray
- break
+ case "cpu":
+ tagArray = cpuTagArray;
+ break;
+ case "mem":
+ tagArray = memTagArray;
+ break;
+ case "swap":
+ tagArray = swapTagArray;
+ break;
}
- tagArray.forEach(
- (tag, index) => {
- let tagDiv = document.createElement('div')
- tagDiv.className = 'device-tag'
- tagDiv.innerText = tag
- // 给最后一个标签不添加后缀
- tagDiv.setAttribute('suffix', index === tagArray.length - 1 ? '0' : '1')
- devices[device].querySelector('.device-tags').appendChild(tagDiv)
- }
- )
+ tagArray.forEach((tag, index) => {
+ let tagDiv = document.createElement("div");
+ tagDiv.className = "device-tag";
+ tagDiv.innerText = tag;
+ // 给最后一个标签不添加后缀
+ tagDiv.setAttribute("suffix", index === tagArray.length - 1 ? "0" : "1");
+ devices[device].querySelector(".device-tags").appendChild(tagDiv);
+ });
}
-
// 插入
- document.getElementById('hardware-info').appendChild(cpuDeviceInfoDiv)
- document.getElementById('hardware-info').appendChild(memDeviceInfoDiv)
- document.getElementById('hardware-info').appendChild(swapDeviceInfoDiv)
+ document.getElementById("hardware-info").appendChild(cpuDeviceInfoDiv);
+ document.getElementById("hardware-info").appendChild(memDeviceInfoDiv);
+ document.getElementById("hardware-info").appendChild(swapDeviceInfoDiv);
- let cpuChart = echarts.init(document.getElementById('cpu-chart'))
- let memChart = echarts.init(document.getElementById('mem-chart'))
- let swapChart = echarts.init(document.getElementById('swap-chart'))
+ let cpuChart = echarts.init(document.getElementById("cpu-chart"));
+ let memChart = echarts.init(document.getElementById("mem-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)}%`, [
- { name: 'used', value: cpuData['percent'] },
- { name: 'free', value: 100 - cpuData['percent'] }
- ]))
-
- memChart.setOption(createPieChartOption(`${localData['memory']}\n${memData['percent'].toFixed(1)}%`, [
- { name: 'process', value: memData['usedProcess'] },
- { 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'] }
- ]))
+ memChart.setOption(
+ createPieChartOption(
+ `${localData["memory"]}\n${memData["percent"].toFixed(1)}%`,
+ [
+ { name: "process", value: memData["usedProcess"] },
+ { 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"] },
+ ]
+ )
+ );
// 磁盘信息
- const diskData = hardwareData['disk']
- diskData.forEach(
- (disk) => {
- let diskTitle = `${disk['name']} ${localData['free']} ${convertSize(disk['free'])} ${localData['total']} ${convertSize(disk['total'])}`
- // 最后一个把margin-bottom去掉
- let diskDiv = createBarChart(diskTitle, disk['percent'])
- if (disk === diskData[diskData.length - 1]) {
- diskDiv.style.marginBottom = '0'
- }
- document.getElementById('disk-info').appendChild(createBarChart(diskTitle, disk['percent']))
- })
+ const diskData = hardwareData["disk"];
+ diskData.forEach((disk) => {
+ let diskTitle = `${localData['free']} ${convertSize(disk['free'])} ${localData['total']} ${convertSize(disk['total'])}`;
+ let diskDiv = createBarChart(diskTitle, disk['percent'], disk['name']);
+ // 最后一个把margin-bottom去掉
+ if (disk === diskData[diskData.length - 1]) {
+ diskDiv.style.marginBottom = "0";
+ }
+ document.getElementById('disk-info').appendChild(diskDiv);
+ });
// 随机一言
- let mottoText = motto_['text']
- let mottoFrom = motto_['source']
- document.getElementById('motto-text').innerText = mottoText
- document.getElementById('motto-from').innerText = mottoFrom
+ let mottoText = motto_["text"];
+ let mottoFrom = motto_["source"];
+ document.getElementById("motto-text").innerText = mottoText;
+ document.getElementById("motto-from").innerText = mottoFrom;
// 致谢
- document.getElementById('addition-info').innerText = '感谢 锅炉 云裳工作室 提供服务器支持'
-
+ document.getElementById("addition-info").innerText =
+ "感谢 锅炉 云裳工作室 提供服务器支持";
}
-main()
\ No newline at end of file
+main();
+/*
+// 窗口大小改变监听器 -- Debug
+window.addEventListener('resize', () => {
+ const diskInfos = document.querySelectorAll('.disk-info');
+ diskInfos.forEach(diskInfo => {
+ updateDiskNameWidth(diskInfo);
+ });
+});
+*/
\ No newline at end of file
diff --git a/src/resources/vanilla_resource/unsorted/plugins.json b/src/resources/vanilla_resource/unsorted/plugins.json
index 89528d6..0ae68c3 100644
--- a/src/resources/vanilla_resource/unsorted/plugins.json
+++ b/src/resources/vanilla_resource/unsorted/plugins.json
@@ -837,22 +837,6 @@
"time": "2023-06-20T16:04:40.706727Z",
"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",
"project_link": "nonebot-plugin-admin",
@@ -8718,31 +8702,6 @@
"time": "2023-07-14T10:32:08.006009Z",
"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",
"project_link": "nonebot-plugin-pokesomeone",
diff --git a/src/utils/__init__.py b/src/utils/__init__.py
index f0ac4ed..d30e26b 100644
--- a/src/utils/__init__.py
+++ b/src/utils/__init__.py
@@ -1,13 +1,9 @@
-import json
-import os.path
-import platform
import sys
-import time
import nonebot
__NAME__ = "尹灵温|轻雪-睿乐"
-__VERSION__ = "6.3.4" # 60201
+__VERSION__ = "6.3.9" # 60201
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 git import Repo
-
major, minor, patch = map(int, __VERSION__.split("."))
__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():
"""
初始化
@@ -64,25 +34,15 @@ def init():
repo = Repo(".")
except Exception as e:
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.data["start_time"] = time.time()
# 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(
- "正在 {} Python{}.{}.{} 上运行 尹灵温".format(
+ "正在 {} Python{}.{}.{} 上运行 尹灵温-NoneBot".format(
sys.executable,
sys.version_info.major,
sys.version_info.minor,
diff --git a/src/utils/base/__init__.py b/src/utils/base/__init__.py
index 5f14052..a0b0a58 100644
--- a/src/utils/base/__init__.py
+++ b/src/utils/base/__init__.py
@@ -1,7 +1,6 @@
import threading
-from nonebot import logger
-from liteyuki.comm.channel import get_channel
+from liteyuki.comm.channel import active_channel
def reload(delay: float = 0.0, receiver: str = "nonebot"):
@@ -14,13 +13,9 @@ def reload(delay: float = 0.0, receiver: str = "nonebot"):
Returns:
"""
- chan = get_channel(receiver + "-active")
- if chan is None:
- logger.error(f"未见 Channel {receiver}-active 以至无法重载")
- return
if delay > 0:
- threading.Timer(delay, chan.send, args=(1,)).start()
+ threading.Timer(delay, active_channel.send, args=(1,)).start()
return
else:
- chan.send(1)
+ active_channel.send(1)
diff --git a/src/utils/base/config.py b/src/utils/base/config.py
index 8e025ab..85737e4 100644
--- a/src/utils/base/config.py
+++ b/src/utils/base/config.py
@@ -1,4 +1,5 @@
import os
+import platform
from typing import List
import nonebot
@@ -7,6 +8,7 @@ from pydantic import BaseModel
from ..message.tools import random_hex_string
+
config = {} # 全局配置,确保加载后读取
@@ -29,23 +31,37 @@ class BasicConfig(BaseModel):
superusers: list[str] = []
command_start: list[str] = ["/", ""]
nickname: list[str] = [f"灵温-{random_hex_string(6)}"]
+ default_language: str = "zh-WY"
satori: SatoriConfig = SatoriConfig()
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
- 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:
+ nonebot.logger.debug("正在从 {} 中加载配置项".format(file_))
+ if not os.path.exists(file_):
+ nonebot.logger.warning(
+ f"未寻得配置文件 {file_} ,已以默认配置创建,请在重启后更改为你所需的内容。"
+ )
+ with open(file_, "w", encoding="utf-8") as f:
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))
config = conf
if conf is None:
- nonebot.logger.warning(f"配置文件 {file} 为空,已创建默认配置,请修改后重启。")
+ nonebot.logger.warning(
+ f"配置文件 {file_} 为空,已以默认配置创建,请在重启后更改为你所需的内容。"
+ )
conf = BasicConfig().dict()
return conf
@@ -75,7 +91,6 @@ def get_config(key: str, default=None):
return default
-
def init_conf(conf: dict) -> dict:
"""
初始化配置文件,确保配置文件中的必要字段存在,且不会冲突
diff --git a/src/utils/base/data.py b/src/utils/base/data.py
index 7536984..c3e3e95 100644
--- a/src/utils/base/data.py
+++ b/src/utils/base/data.py
@@ -30,7 +30,7 @@ class Database:
os.makedirs(os.path.dirname(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._on_save_callbacks = []
@@ -94,7 +94,7 @@ class Database:
f"数据库 Selecting {model.TABLE_NAME} WHERE {condition.replace('?', '%s') % args}"
)
if not table_name:
- raise ValueError(f"数据模型{model_type.__name__}未提供表名")
+ raise ValueError(f"数据模型 {model_type.__name__} 未提供表名")
# condition = f"WHERE {condition}"
# print(f"SELECT * FROM {table_name} {condition}", args)
@@ -118,7 +118,7 @@ class Database:
]
def save(self, *args: LiteModel):
- """增/改操作
+ self.returns_ = """增/改操作
Args:
*args:
Returns:
@@ -126,7 +126,7 @@ class Database:
table_list = [
item[0]
for item in self.cursor.execute(
- "SELECT name FROM sqlite_master WHERE type='table'"
+ "SELECT name FROM sqlite_master WHERE type ='table'"
).fetchall()
]
for model in args:
@@ -158,7 +158,7 @@ class Database:
new_obj[field] = value
else:
raise ValueError(
- f"数据模型{table_name}包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}"
+ f"数据模型 {table_name} 包含不支持的数据类型,字段:{field} 值:{value} 值类型:{type(value)}"
)
if table_name:
fields, values = [], []
@@ -273,9 +273,9 @@ class Database:
"""
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:
- raise ValueError(f"数据模型{model.__class__.__name__}未提供表名")
+ raise ValueError(f"数据模型 {model.__class__.__name__} 未提供表名")
if model.id is not None:
condition = f"id = {model.id}"
if not condition and not allow_empty:
@@ -297,7 +297,7 @@ class Database:
"""
for model in args:
if not model.TABLE_NAME:
- raise ValueError(f"数据模型{type(model).__name__}未提供表名")
+ raise ValueError(f"数据模型 {type(model).__name__} 未提供表名")
# 若无则创建表
self.cursor.execute(
diff --git a/src/utils/base/data_manager.py b/src/utils/base/data_manager.py
index 914f1cf..7b26722 100644
--- a/src/utils/base/data_manager.py
+++ b/src/utils/base/data_manager.py
@@ -2,9 +2,8 @@ import os
from pydantic import Field
-from .data import Database, LiteModel, Database
+from .data import Database, LiteModel
-print("导入数据库模块")
DATA_PATH = "data/liteyuki"
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))
@@ -64,7 +63,7 @@ def auto_migrate():
user_db.auto_migrate(User())
group_db.auto_migrate(Group())
plugin_db.auto_migrate(InstalledPlugin(), GlobalPlugin())
- common_db.auto_migrate(GlobalPlugin(), StoredConfig(), TempConfig())
+ common_db.auto_migrate(GlobalPlugin(), TempConfig())
auto_migrate()
diff --git a/src/utils/base/ly_api.py b/src/utils/base/ly_api.py
deleted file mode 100644
index 5ac2220..0000000
--- a/src/utils/base/ly_api.py
+++ /dev/null
@@ -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()))
diff --git a/src/utils/base/resource.py b/src/utils/base/resource.py
index 717ac41..7aa52d8 100644
--- a/src/utils/base/resource.py
+++ b/src/utils/base/resource.py
@@ -3,7 +3,9 @@ import os
import shutil
import zipfile
from typing import Any
+from pathlib import Path
+# import aiofiles
import nonebot
import yaml
@@ -12,8 +14,8 @@ from .language import Language, get_default_lang_code
from .ly_function import loaded_functions
_loaded_resource_packs: list["ResourceMetadata"] = [] # 按照加载顺序排序
-temp_resource_root = "data/liteyuki/resources"
-temp_extract_root = "data/liteyuki/temp"
+temp_resource_root = Path("data/liteyuki/resources")
+temp_extract_root = Path("data/liteyuki/temp")
lang = Language(get_default_lang_code())
@@ -50,60 +52,139 @@ def load_resource_from_dir(path: str):
for root, dirs, files in os.walk(path):
for file in files:
relative_path = os.path.relpath(os.path.join(root, file), path)
- copy_file(os.path.join(root, file), os.path.join(temp_resource_root, relative_path))
+ copy_file(
+ os.path.join(root, file),
+ os.path.join(temp_resource_root, relative_path),
+ )
metadata["path"] = path
metadata["folder"] = os.path.basename(path)
if os.path.exists(os.path.join(path, "lang")):
# 加载语言
from src.utils.base.language import load_from_dir
+
load_from_dir(os.path.join(path, "lang"))
if os.path.exists(os.path.join(path, "functions")):
# 加载功能
from src.utils.base.ly_function import load_from_dir
+
load_from_dir(os.path.join(path, "functions"))
if os.path.exists(os.path.join(path, "word_bank")):
# 加载词库
from src.utils.base.word_bank import load_from_dir
+
load_from_dir(os.path.join(path, "word_bank"))
_loaded_resource_packs.insert(0, ResourceMetadata(**metadata))
-def get_path(path: str, abs_path: bool = True, default: Any = None, debug: bool = False) -> str | Any:
+def get_path(
+ path: os.PathLike[str,] | Path | str,
+ abs_path: bool = True,
+ default: Any = None,
+ debug: bool = False,
+) -> str | Any:
"""
- 获取资源包中的文件
+ 获取资源包中的路径,且该路径必须存在
Args:
- debug: 启用调试,每次都会先重载资源
+ path: 相对路径
abs_path: 是否返回绝对路径
- default: 默认
- path: 文件相对路径
- Returns: 文件绝对路径
+ default: 默认解,当该路径不存在时使用
+ debug: 启用调试,每次都会先重载资源
+ Returns: 所需求之路径
"""
if debug:
- nonebot.logger.debug("Enable resource debug, Reloading resources")
+ nonebot.logger.debug("由于已启用资源路径调试,正在重载资源")
load_resources()
- resource_relative_path = os.path.join(temp_resource_root, path)
- if os.path.exists(resource_relative_path):
- return os.path.abspath(resource_relative_path) if abs_path else resource_relative_path
+ resource_relative_path = temp_resource_root / path
+ if resource_relative_path.exists():
+ return str(
+ resource_relative_path.resolve() if abs_path else resource_relative_path
+ )
else:
return default
-def get_files(path: str, abs_path: bool = False) -> list[str]:
+def get_resource_path(
+ path: os.PathLike[str,] | Path | str,
+ abs_path: bool = True,
+ only_exist: bool = False,
+ default: Any = None,
+ debug: bool = False,
+) -> Path:
"""
- 获取资源包中一个文件夹的所有文件
+ 获取资源包中的路径
Args:
- abs_path:
- path: 文件夹相对路径
- Returns: 文件绝对路径
+ path: 相对路径
+ abs_path: 是否返回绝对路径
+ only_exist: 检查该路径是否存在
+ default: [当 `only_exist` 为 **真** 时启用]默认解,当该路径不存在时使用
+ debug: 启用调试,每次都会先重载资源
+ Returns: 所需求之路径
"""
- resource_relative_path = os.path.join(temp_resource_root, path)
- if os.path.exists(resource_relative_path):
- return [os.path.abspath(os.path.join(resource_relative_path, file)) if abs_path else os.path.join(resource_relative_path, file) for file in
- os.listdir(resource_relative_path)]
+ if debug:
+ nonebot.logger.debug("由于已启用资源路径调试,正在重载资源")
+ load_resources()
+ resource_relative_path = (
+ (temp_resource_root / path).resolve()
+ if abs_path
+ else (temp_resource_root / path)
+ )
+ if only_exist:
+ if resource_relative_path.exists():
+ return resource_relative_path
+ else:
+ return default
+ else:
+ return resource_relative_path
+
+
+def get_files(
+ path: os.PathLike[str,] | Path | str, abs_path: bool = False
+) -> list[str]:
+ """
+ 获取资源包中一个目录的所有内容
+ Args:
+ path: 该目录的相对路径
+ abs_path: 是否返回绝对路径
+ Returns: 目录内容路径所构成之列表
+ """
+ resource_relative_path = temp_resource_root / path
+ if resource_relative_path.exists():
+ return [
+ (
+ str((resource_relative_path / file_).resolve())
+ if abs_path
+ else str((resource_relative_path / file_))
+ )
+ for file_ in os.listdir(resource_relative_path)
+ ]
+ else:
+ return []
+
+
+def get_resource_files(
+ path: os.PathLike[str,] | Path | str, abs_path: bool = False
+) -> list[Path]:
+ """
+ 获取资源包中一个目录的所有内容
+ Args:
+ path: 该目录的相对路径
+ abs_path: 是否返回绝对路径
+ Returns: 目录内容路径所构成之列表
+ """
+ resource_relative_path = temp_resource_root / path
+ if resource_relative_path.exists():
+ return [
+ (
+ (resource_relative_path / file_).resolve()
+ if abs_path
+ else (resource_relative_path / file_)
+ )
+ for file_ in os.listdir(resource_relative_path)
+ ]
else:
return []
@@ -150,7 +231,9 @@ def load_resources():
if not os.path.exists("resources/index.json"):
json.dump([], open("resources/index.json", "w", encoding="utf-8"))
- resource_index: list[str] = json.load(open("resources/index.json", "r", encoding="utf-8"))
+ resource_index: list[str] = json.load(
+ open("resources/index.json", "r", encoding="utf-8")
+ )
resource_index.reverse() # 优先级高的后加载,但是排在前面
for resource in resource_index:
load_resource_from_dir(os.path.join("resources", resource))
@@ -174,7 +257,9 @@ def check_exist(name: str) -> bool:
Returns: 是否存在
"""
path = os.path.join("resources", name)
- return os.path.exists(os.path.join(path, "metadata.yml")) or (os.path.isfile(path) and name.endswith(".zip"))
+ return os.path.exists(os.path.join(path, "metadata.yml")) or (
+ os.path.isfile(path) and name.endswith(".zip")
+ )
def add_resource_pack(name: str) -> bool:
@@ -185,17 +270,19 @@ def add_resource_pack(name: str) -> bool:
Returns:
"""
if check_exist(name):
- old_index: list[str] = json.load(open("resources/index.json", "r", encoding="utf-8"))
+ old_index: list[str] = json.load(
+ open("resources/index.json", "r", encoding="utf-8")
+ )
if name not in old_index:
old_index.append(name)
json.dump(old_index, open("resources/index.json", "w", encoding="utf-8"))
load_resource_from_dir(os.path.join("resources", name))
return True
else:
- nonebot.logger.warning(lang.get("liteyuki.resource_loaded", name=name))
+ nonebot.logger.warning("资源包 {} 已存在,无需添加".format(name))
return False
else:
- nonebot.logger.warning(lang.get("liteyuki.resource_not_exist", name=name))
+ nonebot.logger.warning("资源包 {} 不存在,无法添加".format(name))
return False
@@ -207,16 +294,18 @@ def remove_resource_pack(name: str) -> bool:
Returns:
"""
if check_exist(name):
- old_index: list[str] = json.load(open("resources/index.json", "r", encoding="utf-8"))
+ old_index: list[str] = json.load(
+ open("resources/index.json", "r", encoding="utf-8")
+ )
if name in old_index:
old_index.remove(name)
json.dump(old_index, open("resources/index.json", "w", encoding="utf-8"))
return True
else:
- nonebot.logger.warning(lang.get("liteyuki.resource_not_loaded", name=name))
+ nonebot.logger.warning("资源包 {} 不存在,无需移除".format(name))
return False
else:
- nonebot.logger.warning(lang.get("liteyuki.resource_not_exist", name=name))
+ nonebot.logger.warning("资源包 {} 不存在,无法移除".format(name))
return False
@@ -229,7 +318,9 @@ def change_priority(name: str, delta: int) -> bool:
Returns:
"""
# 正数表示前移,负数表示后移
- old_resource_list: list[str] = json.load(open("resources/index.json", "r", encoding="utf-8"))
+ old_resource_list: list[str] = json.load(
+ open("resources/index.json", "r", encoding="utf-8")
+ )
new_resource_list = old_resource_list.copy()
if name in old_resource_list:
index = old_resource_list.index(name)
@@ -237,13 +328,15 @@ def change_priority(name: str, delta: int) -> bool:
new_index = index + delta
new_resource_list.remove(name)
new_resource_list.insert(new_index, name)
- json.dump(new_resource_list, open("resources/index.json", "w", encoding="utf-8"))
+ json.dump(
+ new_resource_list, open("resources/index.json", "w", encoding="utf-8")
+ )
return True
else:
- nonebot.logger.warning("Priority change failed, out of range")
+ nonebot.logger.warning("无法更改优先级为 {} ,优先级超出范围".format(delta))
return False
else:
- nonebot.logger.debug("Priority change failed, resource not loaded")
+ nonebot.logger.debug("资源包 {} 未加载,无法更改优先级".format(name))
return False
diff --git a/src/utils/event/get_info.py b/src/utils/event/get_info.py
index 218b6e2..d2d3607 100644
--- a/src/utils/event/get_info.py
+++ b/src/utils/event/get_info.py
@@ -1,6 +1,6 @@
from nonebot.adapters import satori
-
-from src.utils.base.ly_typing import T_MessageEvent
+from nonebot.adapters import onebot
+from src.utils.base.ly_typing import T_MessageEvent, T_GroupMessageEvent
def get_user_id(event: T_MessageEvent):
@@ -10,11 +10,13 @@ def get_user_id(event: T_MessageEvent):
return event.user_id
-def get_group_id(event: T_MessageEvent):
+def get_group_id(event: T_GroupMessageEvent):
if isinstance(event, satori.event.Event):
return event.guild.id
- else:
+ elif isinstance(event, onebot.v11.GroupMessageEvent):
return event.group_id
+ else:
+ return None
def get_message_type(event: T_MessageEvent) -> str:
diff --git a/src/utils/message/html_tool.py b/src/utils/message/html_tool.py
index 8f0cb62..9f43824 100644
--- a/src/utils/message/html_tool.py
+++ b/src/utils/message/html_tool.py
@@ -1,140 +1,20 @@
-import os.path
-
-# import time
-from os import getcwd
-
+import os
import aiofiles
import nonebot
-from nonebot_plugin_htmlrender import * # type: ignore
+from nonebot import require
+
+require("nonebot_plugin_htmlrender")
+
+from nonebot_plugin_htmlrender import (
+ template_to_html,
+ template_to_pic,
+ md_to_pic,
+ init,
+)
+
from .tools import random_hex_string
-# import imgkit
-# from typing import Any, Dict, Literal, Optional, Union
-# import uuid
-
-# import jinja2
-# from pathlib import Path
-
-# TEMPLATES_PATH = str(Path(__file__).parent / "templates")
-# env = jinja2.Environment( # noqa: S701
-# extensions=["jinja2.ext.loopcontrols"],
-# loader=jinja2.FileSystemLoader(TEMPLATES_PATH),
-# enable_async=True,
-# )
-
-
-# async def template_to_html(
-# template_path: str,
-# template_name: str,
-# **kwargs,
-# ) -> str:
-# """使用jinja2模板引擎通过html生成图片
-
-# Args:
-# template_path (str): 模板路径
-# template_name (str): 模板名
-# **kwargs: 模板内容
-# Returns:
-# str: html
-# """
-
-# template_env = jinja2.Environment( # noqa: S701
-# loader=jinja2.FileSystemLoader(template_path),
-# enable_async=True,
-# )
-# template = template_env.get_template(template_name)
-
-# return await template.render_async(**kwargs)
-
-
-# async def template_to_pic(
-# template_path: str,
-# template_name: str,
-# templates: Dict[Any, Any],
-# pages: Optional[Dict[Any, Any]] = None,
-# wait: int = 0,
-# type: Literal["jpeg", "png"] = "png", # noqa: A002
-# quality: Union[int, None] = None,
-# device_scale_factor: float = 2,
-# ) -> bytes:
-# """使用jinja2模板引擎通过html生成图片
-
-# Args:
-# template_path (str): 模板路径
-# template_name (str): 模板名
-# templates (Dict[Any, Any]): 模板内参数 如: {"name": "abc"}
-# pages (Optional[Dict[Any, Any]]): 网页参数 Defaults to
-# {"base_url": f"file://{getcwd()}", "viewport": {"width": 500, "height": 10}}
-# wait (int, optional): 网页载入等待时间. Defaults to 0.
-# type (Literal["jpeg", "png"]): 图片类型, 默认 png
-# quality (int, optional): 图片质量 0-100 当为`png`时无效
-# device_scale_factor: 缩放比例,类型为float,值越大越清晰(真正想让图片清晰更优先请调整此选项)
-# Returns:
-# bytes: 图片 可直接发送
-# """
-# if pages is None:
-# pages = {
-# "viewport": {"width": 500, "height": 10},
-# "base_url": f"file://{getcwd()}", # noqa: PTH109
-# }
-
-# template_env = jinja2.Environment( # noqa: S701
-# loader=jinja2.FileSystemLoader(template_path),
-# enable_async=True,
-# )
-# template = template_env.get_template(template_name)
-
-# open(
-# filename := os.path.join(
-# template_path,
-# str(uuid.uuid4())+".html",
-# ),
-# "w",
-# ).write(await template.render_async(**templates))
-
-# print(pages,filename)
-
-
-# img = imgkit.from_file(
-# filename,
-# output_path=False,
-# options={
-# "format": type,
-# "quality": quality if (quality and type == "jpeg") else 94,
-# "allow": pages["base_url"],
-# # "viewport-size": "{} {}".format(pages["viewport"]["width"],pages["viewport"]["height"]),
-# "zoom": device_scale_factor,
-# # "load-error-handling": "ignore",
-# "enable-local-file-access": None,
-# "no-stop-slow-scripts": None,
-# "transparent": None,
-# },
-# ) # type: ignore
-
-
-# # os.remove(filename)
-
-# return img
-
-# return await html_to_pic(
-# template_path=f"file://{template_path}",
-# html=await template.render_async(**templates),
-# wait=wait,
-# type=type,
-# quality=quality,
-# device_scale_factor=device_scale_factor,
-# **pages,
-# )
-
-
-async def html2image(
- html: str,
- wait: int = 0,
-):
- pass
-
-
async def template2html(
template: str,
templates: dict,
@@ -174,8 +54,8 @@ async def template2image(
if pages is None:
pages = {
"viewport": {"width": 1080, "height": 10},
- "base_url": f"file://{getcwd()}",
}
+
template_path = os.path.dirname(template)
template_name = os.path.basename(template)
@@ -197,36 +77,9 @@ async def template2image(
template_name=template_name,
template_path=template_path,
templates=templates,
- pages=pages,
wait=wait,
+ ###
+ pages=pages,
device_scale_factor=scale_factor,
+ ###
)
-
-
-# async def url2image(
-# url: str,
-# wait: int = 0,
-# scale_factor: float = 1,
-# type: str = "png",
-# quality: int = 100,
-# **kwargs
-# ) -> bytes:
-# """
-# Args:
-# quality:
-# type:
-# url: str: URL
-# wait: int: 等待时间
-# scale_factor: float: 缩放因子
-# **kwargs: page 参数
-# Returns:
-# 图片二进制数据
-# """
-# async with get_new_page(scale_factor) as page:
-# await page.goto(url)
-# await page.wait_for_timeout(wait)
-# return await page.screenshot(
-# full_page=True,
-# type=type,
-# quality=quality
-# )
diff --git a/src/utils/message/message.py b/src/utils/message/message.py
index ca4bd70..100677d 100644
--- a/src/utils/message/message.py
+++ b/src/utils/message/message.py
@@ -1,33 +1,20 @@
import base64
import io
+from typing import Any
from urllib.parse import quote
import aiofiles
-from PIL import Image
import aiohttp
import nonebot
-from nonebot import require
-from nonebot.adapters import satori
+from PIL import Image
from nonebot.adapters.onebot import v11
-from typing import Any, Type
-
-from nonebot.internal.adapter import MessageSegment
-from nonebot.internal.adapter.message import TM
+from .html_tool import md_to_pic
from .. import load_from_yaml
from ..base.ly_typing import T_Bot, T_Message, T_MessageEvent
-require("nonebot_plugin_htmlrender")
-from nonebot_plugin_htmlrender import md_to_pic
-
config = load_from_yaml("config.yml")
-can_send_markdown = {} # 用于存储机器人是否支持发送markdown消息,id->bool
-
-
-class TencentBannedMarkdownError(BaseException):
- pass
-
async def broadcast_to_superusers(message: str | T_Message, markdown: bool = False):
"""广播消息给超级用户"""
@@ -49,9 +36,6 @@ class MarkdownMessage:
*,
message_type: str = None,
session_id: str | int = None,
- event: T_MessageEvent = None,
- retry_as_image: bool = True,
- **kwargs,
) -> dict[str, Any] | None:
"""
发送Markdown消息,支持自动转为图片发送
@@ -60,86 +44,20 @@ class MarkdownMessage:
bot:
message_type:
session_id:
- event:
- retry_as_image: 发送失败后是否尝试以图片形式发送,否则失败返回None
- **kwargs:
-
Returns:
"""
- formatted_md = v11.unescape(markdown).replace("\n", r"\n").replace('"', r"\\\"")
- if event is not None and message_type is None:
- if isinstance(event, satori.event.Event):
- message_type = "private" if event.guild is None else "group"
- group_id = event.guild.id if event.guild is not None else None
- else:
- assert event is not None
- message_type = event.message_type
- group_id = event.group_id if message_type == "group" else None
- user_id = (
- event.user.id
- if isinstance(event, satori.event.Event)
- else event.user_id
- )
- session_id = user_id if message_type == "private" else group_id
-
- # try:
- # raise TencentBannedMarkdownError("Tencent banned markdown")
- # forward_id = await bot.call_api(
- # "send_private_forward_msg",
- # messages=[
- # {
- # "type": "node",
- # "data": {
- # "content": [
- # {
- # "data": {
- # "content": "{\"content\":\"%s\"}" % formatted_md,
- # },
- # "type": "markdown"
- # }
- # ],
- # "name": "[]",
- # "uin": bot.self_id
- # }
- # }
- # ],
- # user_id=bot.self_id
-
- # )
- # data = await bot.send_msg(
- # user_id=session_id,
- # group_id=session_id,
- # message_type=message_type,
- # message=[
- # {
- # "type": "longmsg",
- # "data": {
- # "id": forward_id
- # }
- # },
- # ],
- # **kwargs
- # )
- # except BaseException as e:
-
- nonebot.logger.error(f"因未能发送Markdown消息,已转为图片发送。")
- # 发送失败,渲染为图片发送
- # if not retry_as_image:
- # return None
-
- # plain_markdown = markdown.replace("[🔗", "[")
- md_image_bytes = await md_to_pic(md=markdown, width=540, device_scale_factor=4)
- if isinstance(bot, satori.Bot):
- msg_seg = satori.MessageSegment.image(raw=md_image_bytes, mime="image/png")
- data = await bot.send(event=event, message=msg_seg)
- else:
- data = await bot.send_msg(
- message_type=message_type,
- group_id=session_id,
- user_id=session_id,
- message=v11.MessageSegment.image(md_image_bytes),
- )
+ plain_markdown = markdown.replace("[🔗", "[")
+ md_image_bytes = await md_to_pic(
+ md=plain_markdown, width=540, device_scale_factor=4
+ )
+ print(md_image_bytes)
+ data = await bot.send_msg(
+ message_type=message_type,
+ group_id=session_id,
+ user_id=session_id,
+ message=v11.MessageSegment.image(md_image_bytes),
+ )
return data
@staticmethod
@@ -157,38 +75,25 @@ class MarkdownMessage:
Args:
image: 图片字节流或图片本地路径,链接请使用Markdown.image_async方法获取后通过send_md发送
bot: bot instance
- message_type: message type
+ message_type: message message_type
session_id: session id
event: event
kwargs: other arguments
Returns:
dict: response data
-
"""
if isinstance(image, str):
async with aiofiles.open(image, "rb") as f:
image = await f.read()
method = 2
- # 1.轻雪图床方案
- # if method == 1:
- # image_url = await liteyuki_api.upload_image(image)
- # image_size = Image.open(io.BytesIO(image)).size
- # image_md = Markdown.image(image_url, image_size)
- # data = await Markdown.send_md(image_md, bot, message_type=message_type, session_id=session_id, event=event,
- # retry_as_image=False,
- # **kwargs)
-
- # Lagrange.OneBot方案
if method == 2:
base64_string = base64.b64encode(image).decode("utf-8")
data = await bot.call_api("upload_image", file=f"base64://{base64_string}")
await MarkdownMessage.send_md(
MarkdownMessage.image(data, Image.open(io.BytesIO(image)).size),
bot,
- event=event,
message_type=message_type,
session_id=session_id,
- **kwargs,
)
# 其他实现端方案
@@ -204,12 +109,7 @@ class MarkdownMessage:
image_size = Image.open(io.BytesIO(image)).size
image_md = MarkdownMessage.image(image_url, image_size)
return await MarkdownMessage.send_md(
- image_md,
- bot,
- message_type=message_type,
- session_id=session_id,
- event=event,
- **kwargs,
+ image_md, bot, message_type=message_type, session_id=session_id
)
if data is None:
@@ -293,7 +193,7 @@ class MarkdownMessage:
image = Image.open(io.BytesIO(await resp.read()))
return MarkdownMessage.image(url, image.size)
except Exception as e:
- nonebot.logger.error(f"get image error: {e}")
+ nonebot.logger.error(f"获取图片错误:{e}")
return "[Image Error]"
@staticmethod
diff --git a/tests/test_config_load.py b/tests/test_config_load.py
new file mode 100644
index 0000000..d4c3039
--- /dev/null
+++ b/tests/test_config_load.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+"""
+Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
+
+@Time : 2024/8/11 下午11:07
+@Author : snowykami
+@Email : snowykami@outlook.com
+@File : test_config_load.py
+@Software: PyCharm
+"""
+import json
+import os
+import sys
+
+sys.path.insert(0, os.getcwd())
+from liteyuki.config import load_config_in_default
+
+
+def test_default_load():
+ config = load_config_in_default()
+ print(json.dumps(config, indent=4, ensure_ascii=False))
diff --git a/tests/test_core.py b/tests/test_core.py
index 81da20e..b8983b0 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -1,4 +1,4 @@
-from src.liteyuki import LiteyukiBot
+from liteyuki import LiteyukiBot
if __name__ == "__main__":
lyb = LiteyukiBot()
diff --git a/tests/test_dll.py b/tests/test_dll.py
deleted file mode 100644
index 8d44378..0000000
--- a/tests/test_dll.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
-
-@Time : 2024/8/7 下午11:44
-@Author : snowykami
-@Email : snowykami@outlook.com
-@File : test_dll.py
-@Software: PyCharm
-"""
-from src.utils.extension import load_lib
-
-
-a = load_lib("src/libs/ly_api")
-
-a.Register("sss", "sss", 64, "sss", "sss")
\ No newline at end of file
diff --git a/unused_resource/liteyuki_weather/templates/js/weather_now.js b/unused_resource/liteyuki_weather/templates/js/weather_now.js
index b39552f..5fc41c0 100644
--- a/unused_resource/liteyuki_weather/templates/js/weather_now.js
+++ b/unused_resource/liteyuki_weather/templates/js/weather_now.js
@@ -180,4 +180,4 @@ function get_time_hour(fxTime) {
let attrinfo = document.getElementById('attribution-info');
-attrinfo.innerText = is_dev ? "Weather Service Drived by QWeather" : (attr ? attr : "Weather Service Drived by QWeather");
+attrinfo.innerText = is_dev ? "Weather service by QWeather" : (attr ? attr : "Weather Service Drived by QWeather");