Compare commits

...

15 Commits

Author SHA1 Message Date
Soulter 542d4bc703 typo: fix t2i typo 2024-06-03 08:47:51 -04:00
Soulter e3640fdac9 perf: 优化update、help等指令的输出效果 2024-06-03 08:33:17 -04:00
Soulter f64ab4b190 chore: 移除了一些过时的方法 2024-06-03 05:54:40 -04:00
Soulter bd571e1577 feat: 提供新的文本转图片样式 2024-06-03 05:51:44 -04:00
Soulter e4a5cbd893 prof: 改善加载插件时的稳定性 2024-06-03 00:20:56 -04:00
Soulter 7a9fd7fd1e fix: 修复报配置文件未找到的问题 2024-06-02 23:14:48 -04:00
Soulter d9b60108db Update README.md 2024-05-30 18:11:57 +08:00
Soulter 8455c8b4ed Update README.md 2024-05-30 18:03:59 +08:00
Soulter 5c2e7099fc Update README.md 2024-05-26 21:38:32 +08:00
Soulter 1fd1d55895 Update config.py 2024-05-26 21:31:26 +08:00
Soulter 5ce4137e75 fix: 修复model指令 2024-05-26 21:15:33 +08:00
Soulter d49179541e feat: 给插件的init方法传入 ctx 2024-05-26 21:10:19 +08:00
Soulter 676f258981 perf: 重启后终止子进程 2024-05-26 21:09:23 +08:00
Soulter fa44749240 fix: 修复相对路径导致的windows启动器无法安装依赖的问题 2024-05-26 18:15:25 +08:00
Soulter 6c856f9da2 fix(typo): 修复插件注册器的一个typo导致无法注册消息平台插件的问题 2024-05-26 18:07:07 +08:00
18 changed files with 510 additions and 239 deletions
+15 -38
View File
@@ -7,62 +7,37 @@
# AstrBot # AstrBot
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot)](https://github.com/Soulter/AstrBot/releases/latest) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot)](https://github.com/Soulter/AstrBot/releases/latest)
<img src="https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/34412545-2e37-400f-bedc-42348713ac1f.svg" alt="wakatime">
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python"> <img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a> <a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft"> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft">
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-322154837-purple"> <img alt="Static Badge" src="https://img.shields.io/badge/QQ群-322154837-purple">
</a> </a>
<img alt="Static Badge" src="https://img.shields.io/badge/频道-x42d56aki2-purple">
<a href="https://astrbot.soulter.top/center">项目部署</a> <a href="https://astrbot.soulter.top/center">项目部署</a>
<a href="https://github.com/Soulter/QQChannelChatGPT/issues">问题提交</a> <a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
<a href="https://astrbot.soulter.top/center/docs/%E5%BC%80%E5%8F%91/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91">插件开发(最少只需 25 行)</a> <a href="https://astrbot.soulter.top/center/docs/%E5%BC%80%E5%8F%91/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91">插件开发</a>
</div> </div>
## 🤔您可能想了解的
- **如何部署?** [帮助文档](https://astrbot.soulter.top/center/docs/%E9%83%A8%E7%BD%B2/%E9%80%9A%E8%BF%87Docker%E9%83%A8%E7%BD%B2) (部署不成功欢迎进群捞人解决<3)
- **go-cqhttp启动不成功、报登录失败?** [在这里搜索解决方法](https://github.com/Mrs4s/go-cqhttp/issues)
- **程序闪退/机器人启动不成功?** [提交issue或加群反馈](https://github.com/Soulter/QQChannelChatGPT/issues)
- **如何开启 ChatGPT、Claude、HuggingChat 等语言模型?** [查看帮助](https://astrbot.soulter.top/center/docs/%E4%BD%BF%E7%94%A8/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B)
## 🧩功能: ## 🧩功能:
✨ 最近功能: 🌍支持的消息平台
1. 可视化面板 - QQ 群、QQ 频道(OneBot、QQ 官方接口)
2. Docker 一键部署项目:[链接](https://astrbot.soulter.top/center/docs/%E9%83%A8%E7%BD%B2/%E9%80%9A%E8%BF%87Docker%E9%83%A8%E7%BD%B2)
🌍支持的消息平台/接口
- go-cqhttpQQ、QQ频道)
- QQ 官方机器人接口
- Telegram(由 [astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持) - Telegram(由 [astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持)
🌍支持的AI语言模型一览: 🌍支持的模型一览:
**文字模型/图片理解** - OpenAI GPT、DallE 系列
- OpenAI GPT-3(原生支持)
- OpenAI GPT-3.5(原生支持)
- OpenAI GPT-4(原生支持)
- Claude(免费,由[LLMs插件](https://github.com/Soulter/llms)支持) - Claude(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
- HuggingChat(免费,由[LLMs插件](https://github.com/Soulter/llms)支持) - HuggingChat(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
- Gemini(免费,由[LLMs插件](https://github.com/Soulter/llms)支持) - Gemini(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
**图片生成**
- OpenAI Dalle 接口
- NovelAI/Naifu (免费,由[AIDraw插件](https://github.com/Soulter/aidraw)支持)
🌍机器人支持的能力一览: 🌍机器人支持的能力一览:
- 可视化面板(beta - 大模型对话、人格、网页搜索
- 同时部署机器人到 QQ 和 QQ 频道 - 可视化管理面板
- 大模型对话 - 同时处理多平台消息
- 大模型网页搜索能力 **(目前仅支持OpenAI系模型,最新版本下使用 web on 指令打开)** - 精确到个人的会话隔离
- 插件(在QQ或QQ频道聊天框内输入 `plugin` 了解详情) - 插件支持
- 回复文字图片渲染(以图片markdown格式回复,**大幅度降低被风控概率**,需手动在`cmd_config.json`内开启qq_pic_mode - 文本转图片回复
- 人格设置
- 关键词回复
- 热更新(更新本项目时**仅需**在QQ或QQ频道聊天框内输入`update latest r`
- Windows一键部署 https://github.com/Soulter/QQChatGPTLauncher/releases/latest
<!-- <!--
### 基本功能 ### 基本功能
@@ -133,7 +108,7 @@
- `LLMS`: https://github.com/Soulter/llms | Claude, HuggingChat 大语言模型接入。 - `LLMS`: https://github.com/Soulter/llms | Claude, HuggingChat 大语言模型接入。
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等 - `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等
- `sysstat`: https://github.com/Soulter/sysstatqcbot | 查看系统状态 - `sysstat`: https://github.com/Soulter/sysstatqcbot | 查看系统状态
@@ -141,6 +116,8 @@
- `liferestart`: https://github.com/Soulter/liferestart | 人生重开模拟器 - `liferestart`: https://github.com/Soulter/liferestart | 人生重开模拟器
- `astrbot_plugin_aiocqhttp`: https://github.com/Soulter/astrbot_plugin_aiocqhttp | aiocqhttp 适配器,支持接入支持反向 WS 的 OneBot 协议实现,如 Lagrange.OneBotShamrock 等。
<img width="900" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/824d1ff3-7b85-481c-b795-8e62dedb9fd7"> <img width="900" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/824d1ff3-7b85-481c-b795-8e62dedb9fd7">
+4 -5
View File
@@ -16,7 +16,7 @@ from persist.session import dbConn
from type.register import RegisteredPlugin from type.register import RegisteredPlugin
from typing import List from typing import List
from util.cmd_config import CmdConfig from util.cmd_config import CmdConfig
from util.updator import check_update, update_project, request_release_info from util.updator import check_update, update_project, request_release_info, _reboot
from SparkleLogging.utils.core import LogManager from SparkleLogging.utils.core import LogManager
from logging import Logger from logging import Logger
logger: Logger = LogManager.GetLogger(log_name='astrbot-core') logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
@@ -196,7 +196,7 @@ class AstrBotDashBoard():
repo_url = post_data["url"] repo_url = post_data["url"]
try: try:
logger.info(f"正在安装插件 {repo_url}") logger.info(f"正在安装插件 {repo_url}")
putil.install_plugin(repo_url, self.dashboard_data.plugins) putil.install_plugin(repo_url, global_object)
logger.info(f"安装插件 {repo_url} 成功") logger.info(f"安装插件 {repo_url} 成功")
return Response( return Response(
status="success", status="success",
@@ -237,7 +237,7 @@ class AstrBotDashBoard():
plugin_name = post_data["name"] plugin_name = post_data["name"]
try: try:
logger.info(f"正在更新插件 {plugin_name}") logger.info(f"正在更新插件 {plugin_name}")
putil.update_plugin(plugin_name, self.dashboard_data.plugins) putil.update_plugin(plugin_name, global_object)
logger.info(f"更新插件 {plugin_name} 成功") logger.info(f"更新插件 {plugin_name} 成功")
return Response( return Response(
status="success", status="success",
@@ -344,8 +344,7 @@ class AstrBotDashBoard():
def shutdown_bot(self, delay_s: int): def shutdown_bot(self, delay_s: int):
time.sleep(delay_s) time.sleep(delay_s)
py = sys.executable _reboot()
os.execl(py, py, *sys.argv)
def _get_configs(self, namespace: str): def _get_configs(self, namespace: str):
if namespace == "": if namespace == "":
+2 -7
View File
@@ -83,6 +83,7 @@ def init():
_global_object = GlobalObject() _global_object = GlobalObject()
_global_object.version = VERSION _global_object.version = VERSION
_global_object.base_config = cfg _global_object.base_config = cfg
_global_object.logger = logger
logger.info("AstrBot v" + VERSION) logger.info("AstrBot v" + VERSION)
if 'reply_prefix' in cfg: if 'reply_prefix' in cfg:
@@ -171,7 +172,7 @@ def init():
logger.info("正在载入插件...") logger.info("正在载入插件...")
# 加载插件 # 加载插件
_command = Command(None, _global_object) _command = Command(None, _global_object)
ok, err = putil.plugin_reload(_global_object.cached_plugins) ok, err = putil.plugin_reload(_global_object)
if ok: if ok:
logger.info( logger.info(
f"成功载入 {len(_global_object.cached_plugins)} 个插件") f"成功载入 {len(_global_object.cached_plugins)} 个插件")
@@ -441,12 +442,6 @@ async def oper_msg(message: AstrBotMessage,
return return
command = command_result[2] command = command_result[2]
if command == "update latest r":
def update_restart():
py = sys.executable
os.execl(py, py, *sys.argv)
return MessageResult(command_result[1] + "\n\n即将自动重启。", callback=update_restart)
if not command_result[0]: if not command_result[0]:
return MessageResult(f"指令调用错误: \n{str(command_result[1])}") return MessageResult(f"指令调用错误: \n{str(command_result[1])}")
+4 -6
View File
@@ -8,10 +8,8 @@ from logging import Formatter, Logger
from util.cmd_config import CmdConfig, try_migrate_config from util.cmd_config import CmdConfig, try_migrate_config
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
abs_path = os.path.dirname(os.path.realpath(sys.argv[0])) + '/'
logger: Logger = None logger: Logger = None
logo_tmpl = """ logo_tmpl = """
___ _______.___________..______ .______ ______ .___________. ___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | | / \ / | || _ \ | _ \ / __ \ | |
@@ -35,9 +33,11 @@ def update_dept():
''' '''
# 获取 Python 可执行文件路径 # 获取 Python 可执行文件路径
py = sys.executable py = sys.executable
requirements_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")
print(requirements_path)
# 更新依赖库 # 更新依赖库
mirror = "https://mirrors.aliyun.com/pypi/simple/" mirror = "https://mirrors.aliyun.com/pypi/simple/"
os.system(f"{py} -m pip install -r requirements.txt -i {mirror}") os.system(f"{py} -m pip install -r {requirements_path} -i {mirror}")
def main(): def main():
try: try:
@@ -65,8 +65,6 @@ def main():
input("未知错误。") input("未知错误。")
exit() exit()
make_necessary_dirs()
# 启动主程序(cores/qqbot/core.py # 启动主程序(cores/qqbot/core.py
bot_core.init() bot_core.init()
@@ -79,7 +77,7 @@ def check_env():
if __name__ == "__main__": if __name__ == "__main__":
update_dept() update_dept()
make_necessary_dirs()
try_migrate_config() try_migrate_config()
cc = CmdConfig() cc = CmdConfig()
http_proxy = cc.get("http_proxy") http_proxy = cc.get("http_proxy")
+40 -42
View File
@@ -11,6 +11,7 @@ from nakuru.entities.components import (
Image Image
) )
from util import general_utils as gu from util import general_utils as gu
from util.image_render.helper import text_to_image_base
from model.provider.provider import Provider from model.provider.provider import Provider
from util.cmd_config import CmdConfig as cc from util.cmd_config import CmdConfig as cc
from type.message import * from type.message import *
@@ -97,14 +98,16 @@ class Command:
if self.command_start_with(message, "nick"): if self.command_start_with(message, "nick"):
return True, self.set_nick(message, platform, role) return True, self.set_nick(message, platform, role)
if self.command_start_with(message, "plugin"): if self.command_start_with(message, "plugin"):
return True, self.plugin_oper(message, role, cached_plugins, platform) return True, await self.plugin_oper(message, role, self.global_object, platform)
if self.command_start_with(message, "myid") or self.command_start_with(message, "!myid"): if self.command_start_with(message, "myid") or self.command_start_with(message, "!myid"):
return True, self.get_my_id(message_obj, platform) return True, self.get_my_id(message_obj, platform)
if self.command_start_with(message, "web"): # 网页搜索 if self.command_start_with(message, "web"): # 网页搜索
return True, self.web_search(message) return True, self.web_search(message)
if self.command_start_with(message, "update"): if self.command_start_with(message, "update"):
return True, self.update(message, role) return True, self.update(message, role)
if not self.provider and self.command_start_with(message, "help"): if message == "t2i":
return True, "t2i", self.t2i_toggle(message, role)
if not self.provider and message == "help":
return True, await self.help() return True, await self.help()
return False, None return False, None
@@ -119,40 +122,34 @@ class Command:
elif l[1] == 'off': elif l[1] == 'off':
self.global_object.web_search = False self.global_object.web_search = False
return True, "已关闭网页搜索", "web" return True, "已关闭网页搜索", "web"
def t2i_toggle(self, message, role):
p = cc.get("qq_pic_mode", True)
if p:
cc.put("qq_pic_mode", False)
return True, "已关闭文本转图片模式。", "t2i"
cc.put("qq_pic_mode", True)
return True, "已开启文本转图片模式。", "t2i"
def get_my_id(self, message_obj, platform): def get_my_id(self, message_obj, platform):
try: try:
user_id = str(message_obj.user_id) user_id = str(message_obj.sender.user_id)
return True, f"你在此平台上的ID{user_id}", "plugin" return True, f"你在此平台上的ID{user_id}", "plugin"
except BaseException as e: except BaseException as e:
return False, f"{platform}上获取你的ID失败,原因: {str(e)}", "plugin" return False, f"{platform}上获取你的ID失败,原因: {str(e)}", "plugin"
def get_new_conf(self, message, role): async def plugin_oper(self, message: str, role: str, ctx: GlobalObject, platform: str):
if role != "admin":
return False, f"你的身份组{role}没有权限使用此指令。", "newconf"
l = message.split(" ")
if len(l) <= 1:
obj = cc.get_all()
p = gu.create_text_image("【cmd_config.json】", json.dumps(
obj, indent=4, ensure_ascii=False))
return True, [Image.fromFileSystem(p)], "newconf"
'''
插件指令
'''
def plugin_oper(self, message: str, role: str, cached_plugins: List[RegisteredPlugin], platform: str):
l = message.split(" ") l = message.split(" ")
if len(l) < 2: if len(l) < 2:
p = gu.create_text_image( p = await text_to_image_base("# 插件指令面板 \n- 安装插件: `plugin i 插件Github地址`\n- 卸载插件: `plugin d 插件名`\n- 重载插件: `plugin reload`\n- 查看插件列表:`plugin l`\n - 更新插件: `plugin u 插件名`\n")
"【插件指令面板】", "安装插件: \nplugin i 插件Github地址\n卸载插件: \nplugin d 插件名 \n重载插件: \nplugin reload\n查看插件列表:\nplugin l\n更新插件: plugin u 插件名\n") with open(p, 'rb') as f:
return True, [Image.fromFileSystem(p)], "plugin" return True, [Image.fromBytes(f.read())], "plugin"
else: else:
if l[1] == "i": if l[1] == "i":
if role != "admin": if role != "admin":
return False, f"你的身份组{role}没有权限安装插件", "plugin" return False, f"你的身份组{role}没有权限安装插件", "plugin"
try: try:
putil.install_plugin(l[2], cached_plugins) putil.install_plugin(l[2], )
return True, "插件拉取并载入成功~", "plugin" return True, "插件拉取并载入成功~", "plugin"
except BaseException as e: except BaseException as e:
return False, f"拉取插件失败,原因: {str(e)}", "plugin" return False, f"拉取插件失败,原因: {str(e)}", "plugin"
@@ -160,37 +157,37 @@ class Command:
if role != "admin": if role != "admin":
return False, f"你的身份组{role}没有权限删除插件", "plugin" return False, f"你的身份组{role}没有权限删除插件", "plugin"
try: try:
putil.uninstall_plugin(l[2], cached_plugins) putil.uninstall_plugin(l[2], ctx)
return True, "插件卸载成功~", "plugin" return True, "插件卸载成功~", "plugin"
except BaseException as e: except BaseException as e:
return False, f"卸载插件失败,原因: {str(e)}", "plugin" return False, f"卸载插件失败,原因: {str(e)}", "plugin"
elif l[1] == "u": elif l[1] == "u":
try: try:
putil.update_plugin(l[2], cached_plugins) putil.update_plugin(l[2], ctx)
return True, "\n更新插件成功!!", "plugin" return True, "\n更新插件成功!!", "plugin"
except BaseException as e: except BaseException as e:
return False, f"更新插件失败,原因: {str(e)}\n建议: 使用 plugin i 指令进行覆盖安装(插件数据可能会丢失)", "plugin" return False, f"更新插件失败,原因: {str(e)}\n建议: 使用 plugin i 指令进行覆盖安装(插件数据可能会丢失)", "plugin"
elif l[1] == "l": elif l[1] == "l":
try: try:
plugin_list_info = "" plugin_list_info = ""
for plugin in cached_plugins: for plugin in ctx.cached_plugins:
plugin_list_info += f"{plugin.metadata.plugin_name}: \n名称: {plugin.metadata.plugin_name}\n简介: {plugin.metadata.plugin_desc}\n版本: {plugin.metadata.version}\n作者: {plugin.metadata.author}\n" plugin_list_info += f"### {plugin.metadata.plugin_name} \n- 名称: {plugin.metadata.plugin_name}\n- 简介: {plugin.metadata.desc}\n- 版本: {plugin.metadata.version}\n- 作者: {plugin.metadata.author}\n"
p = gu.create_text_image( p = await text_to_image_base(f"# 已激活的插件\n{plugin_list_info}\n> 使用plugin v 插件名 查看插件帮助\n")
"【已激活插件列表】", plugin_list_info + "\n使用plugin v 插件名 查看插件帮助\n") with open(p, 'rb') as f:
return True, [Image.fromFileSystem(p)], "plugin" return True, [Image.fromBytes(f.read())], "plugin"
except BaseException as e: except BaseException as e:
return False, f"获取插件列表失败,原因: {str(e)}", "plugin" return False, f"获取插件列表失败,原因: {str(e)}", "plugin"
elif l[1] == "v": elif l[1] == "v":
try: try:
info = None info = None
for i in cached_plugins: for i in ctx.cached_plugins:
if i.metadata.plugin_name == l[2]: if i.metadata.plugin_name == l[2]:
info = i.metadata info = i.metadata
break break
if info: if info:
p = gu.create_text_image( p = await text_to_image_base(f"# `{info.plugin_name}` 插件信息\n- 类型: {info.plugin_type}\n- 简介{info.desc}\n- 版本: {info.version}\n- 作者: {info.author}")
f"【插件信息】", f"名称: {info.plugin_name}\n类型: {info.plugin_type}\n{info.desc}\n版本: {info.version}\n作者: {info.author}") with open(p, 'rb') as f:
return True, [Image.fromFileSystem(p)], "plugin" return True, [Image.fromBytes(f.read())], "plugin"
else: else:
return False, "未找到该插件", "plugin" return False, "未找到该插件", "plugin"
except BaseException as e: except BaseException as e:
@@ -232,22 +229,23 @@ class Command:
notice = (await resp.json())["notice"] notice = (await resp.json())["notice"]
except BaseException as e: except BaseException as e:
notice = "" notice = ""
msg = "# Help Center\n## 指令列表\n" msg = "## 指令列表\n"
for key, value in commands.items(): for key, value in commands.items():
msg += f"`{key}` - {value}\n" msg += f"- `{key}`: {value}\n"
# plugins # plugins
if cached_plugins != None: if cached_plugins:
plugin_list_info = "" plugin_list_info = ""
for plugin in cached_plugins: for plugin in cached_plugins:
plugin_list_info += f"`{plugin.metadata.plugin_name}` {plugin.metadata.desc}\n" plugin_list_info += f"- `{plugin.metadata.plugin_name}`: {plugin.metadata.desc}\n"
if plugin_list_info.strip() != "": if plugin_list_info.strip():
msg += "\n## 插件列表\n> 使用plugin v 插件名 查看插件帮助\n" msg += "\n## 插件列表\n> 使用 plugin v 插件名 查看插件帮助\n"
msg += plugin_list_info msg += plugin_list_info
msg += notice msg += notice
try: try:
p = gu.create_markdown_image(msg) p = await text_to_image_base(msg)
return [Image.fromFileSystem(p),] with open(p, 'rb') as f:
return [Image.fromBytes(f.read()),]
except BaseException as e: except BaseException as e:
logger.error(str(e)) logger.error(str(e))
return msg return msg
@@ -268,7 +266,7 @@ class Command:
if len(l) == 1: if len(l) == 1:
try: try:
update_info = util.updator.check_update() update_info = util.updator.check_update()
update_info += "\nTips:\n输入「update latest」更新到最新版本\n输入「update <版本号如v3.1.3>」切换到指定版本\n输入「update r」重启机器人\n" update_info += "\n> Tips: 输入「update latest」更新到最新版本输入「update <版本号如v3.1.3>」切换到指定版本输入「update r」重启机器人\n"
return True, update_info, "update" return True, update_info, "update"
except BaseException as e: except BaseException as e:
return False, "检查更新失败: "+str(e), "update" return False, "检查更新失败: "+str(e), "update"
+10 -7
View File
@@ -94,14 +94,17 @@ class CommandOpenAIOfficial(Command):
if len(l) == 1: if len(l) == 1:
return True, "请输入 /model 模型名/编号", "model" return True, "请输入 /model 模型名/编号", "model"
model = str(l[1]) model = str(l[1])
models = await self.get_models() if model.isdigit():
models = list(models) models = await self.get_models()
if model.isdigit() and int(model) <= len(models) and int(model) >= 1: models = list(models)
model = models[int(model)-1] if int(model) <= len(models) and int(model) >= 1:
model = models[int(model)-1]
self.provider.set_model(model.id)
return True, f"模型已设置为 {model.id}", "model"
else:
self.provider.set_model(model)
return True, f"模型已设置为 {model} (自定义)", "model"
self.provider.set_model(model.id)
return True, f"模型已设置为 {model.id}", "model"
async def help(self): async def help(self):
commands = super().general_commands() commands = super().general_commands()
+8 -4
View File
@@ -7,7 +7,8 @@ from nakuru import (
import botpy.message import botpy.message
from type.message import * from type.message import *
from typing import List, Union from typing import List, Union
import time from util.general_utils import save_temp_img
import time, base64
# QQ官方消息类型转换 # QQ官方消息类型转换
@@ -18,11 +19,14 @@ def qq_official_message_parse(message: List[BaseMessageComponent]):
for i in message: for i in message:
if isinstance(i, Plain): if isinstance(i, Plain):
plain_text += i.text plain_text += i.text
elif isinstance(i, Image) and image_path == None: elif isinstance(i, Image) and not image_path:
if i.path is not None: if i.path:
image_path = i.path image_path = i.path
elif i.file and i.file.startswith("base64://"):
img_data = base64.b64decode(i.file[9:])
image_path = save_temp_img(img_data)
else: else:
image_path = i.file image_path = save_temp_img(i.file)
return plain_text, image_path return plain_text, image_path
# QQ官方消息类型 2 AstrBotMessage # QQ官方消息类型 2 AstrBotMessage
+5 -9
View File
@@ -1,5 +1,6 @@
from nakuru.entities.components import Plain, At, Image, Node from nakuru.entities.components import Plain, At, Image, Node
from util import general_utils as gu from util import general_utils as gu
from util.image_render.helper import text_to_image_base
from util.cmd_config import CmdConfig from util.cmd_config import CmdConfig
import asyncio import asyncio
from nakuru import ( from nakuru import (
@@ -213,7 +214,8 @@ class QQGOCQ(Platform):
news.append(i) news.append(i)
plains_str = "".join(plains).strip() plains_str = "".join(plains).strip()
if plains_str != "" and len(plains_str) > 50: if plains_str != "" and len(plains_str) > 50:
p = gu.create_markdown_image("".join(plains)) # p = gu.create_markdown_image("".join(plains))
p = await text_to_image_base(plains_str)
news.append(Image.fromFileSystem(p)) news.append(Image.fromFileSystem(p))
res = news res = news
@@ -271,20 +273,14 @@ class QQGOCQ(Platform):
await super().reply_msg() await super().reply_msg()
await self.reply_msg(to, res) await self.reply_msg(to, res)
def create_text_image(title: str, text: str, max_width=30, font_size=20): async def create_text_image(text: str):
''' '''
文本转图片。 文本转图片。
title: 标题
text: 文本内容 text: 文本内容
max_width: 文本宽度最大值(默认30)
font_size: 字体大小(默认20
返回:文件路径 返回:文件路径
''' '''
try: try:
img = gu.word2img(title, text, max_width, font_size) return await text_to_image_base(text)
p = gu.save_temp_img(img)
return p
except Exception as e: except Exception as e:
raise e raise e
+49 -25
View File
@@ -19,7 +19,8 @@ from ._message_parse import (
) )
from type.message import * from type.message import *
from typing import Union, List from typing import Union, List
from nakuru.entities.components import BaseMessageComponent from nakuru.entities.components import *
from util.image_render.helper import text_to_image_base
from SparkleLogging.utils.core import LogManager from SparkleLogging.utils.core import LogManager
from logging import Logger from logging import Logger
@@ -64,6 +65,7 @@ class QQOfficial(Platform):
self.secret = cfg['qqbot_secret'] self.secret = cfg['qqbot_secret']
self.unique_session = cfg['uniqueSessionMode'] self.unique_session = cfg['uniqueSessionMode']
qq_group = cfg['qqofficial_enable_group_message'] qq_group = cfg['qqofficial_enable_group_message']
self.pic_mode = cfg['qq_pic_mode']
if qq_group: if qq_group:
self.intents = botpy.Intents( self.intents = botpy.Intents(
@@ -169,32 +171,54 @@ class QQOfficial(Platform):
image_path = '' image_path = ''
msg_ref = None msg_ref = None
# if isinstance(res, list):
# plain_text, image_path = qq_official_message_parse(res)
# elif isinstance(res, str):
# plain_text = res
# if self.cfg['qq_pic_mode']:
# # 文本转图片,并且加上原来的图片
# if plain_text != '' or image_path != '':
# if image_path is not None and image_path != '':
# if image_path.startswith("http"):
# plain_text += "\n\n" + "![](" + image_path + ")"
# else:
# plain_text += "\n\n" + \
# "![](file:///" + image_path + ")"
# # image_path = gu.create_markdown_image("".join(plain_text))
# image_path = await text_to_image_base("".join(plain_text))
# plain_text = ""
# else:
# if image_path is not None and image_path != '':
# msg_ref = None
# if image_path.startswith("http"):
# async with aiohttp.ClientSession() as session:
# async with session.get(image_path) as response:
# if response.status == 200:
# image = PILImage.open(io.BytesIO(await response.read()))
# image_path = gu.save_temp_img(image)
if self.pic_mode:
plains = []
news = []
if isinstance(res, str):
res = [Plain(text=res, convert=False),]
for i in res:
if isinstance(i, Plain):
plains.append(i.text)
else:
news.append(i)
plains_str = "".join(plains).strip()
if plains_str and len(plains_str) > 50:
p = await text_to_image_base(plains_str, return_url=False)
with open(p, "rb") as f:
news.append(Image.fromBytes(f.read()))
res = news
if isinstance(res, list): if isinstance(res, list):
plain_text, image_path = qq_official_message_parse(res) plain_text, image_path = qq_official_message_parse(res)
elif isinstance(res, str):
plain_text = res
if self.cfg['qq_pic_mode']:
# 文本转图片,并且加上原来的图片
if plain_text != '' or image_path != '':
if image_path is not None and image_path != '':
if image_path.startswith("http"):
plain_text += "\n\n" + "![](" + image_path + ")"
else:
plain_text += "\n\n" + \
"![](file:///" + image_path + ")"
image_path = gu.create_markdown_image("".join(plain_text))
plain_text = ""
else: else:
if image_path is not None and image_path != '': plain_text = res
msg_ref = None
if image_path.startswith("http"):
async with aiohttp.ClientSession() as session:
async with session.get(image_path) as response:
if response.status == 200:
image = PILImage.open(io.BytesIO(await response.read()))
image_path = gu.save_temp_img(image)
if source is not None and image_path == '': # file_image与message_reference不能同时传入 if source is not None and image_path == '': # file_image与message_reference不能同时传入
msg_ref = Reference(message_id=source.id, msg_ref = Reference(message_id=source.id,
@@ -215,7 +239,7 @@ class QQOfficial(Platform):
data['guild_id'] = source.guild_id data['guild_id'] = source.guild_id
else: else:
raise ValueError(f"未知的消息类型: {message.type}") raise ValueError(f"未知的消息类型: {message.type}")
if image_path != '': if image_path:
data['file_image'] = image_path data['file_image'] = image_path
try: try:
+1 -1
View File
@@ -1 +1 @@
VERSION = '3.1.13' VERSION = '3.2.4'
+1 -1
View File
@@ -2,7 +2,7 @@ from enum import Enum
from dataclasses import dataclass from dataclasses import dataclass
class PluginType(Enum): class PluginType(Enum):
PLATFORM = 'platfrom' # 平台类插件。 PLATFORM = 'platform' # 平台类插件。
LLM = 'llm' # 大语言模型类插件 LLM = 'llm' # 大语言模型类插件
COMMON = 'common' # 其他插件 COMMON = 'common' # 其他插件
+4
View File
@@ -1,5 +1,7 @@
from type.register import * from type.register import *
from typing import List from typing import List
from logging import Logger
class GlobalObject: class GlobalObject:
''' '''
@@ -17,6 +19,8 @@ class GlobalObject:
unique_session: bool # 是否开启了独立会话 unique_session: bool # 是否开启了独立会话
default_personality: dict default_personality: dict
dashboard_data = None dashboard_data = None
logger: Logger = None
def __init__(self): def __init__(self):
self.nick = None # gocq 的昵称 self.nick = None # gocq 的昵称
+14 -75
View File
@@ -10,6 +10,7 @@ import json
import sys import sys
import psutil import psutil
import ssl import ssl
import base64
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from type.types import GlobalObject from type.types import GlobalObject
@@ -46,42 +47,6 @@ def get_font_path() -> str:
raise Exception("找不到字体文件") raise Exception("找不到字体文件")
return font_path return font_path
def word2img(title: str, text: str, max_width=30, font_size=20):
font_path = get_font_path()
width_factor = 1.0
height_factor = 1.5
# 格式化文本宽度最大为30
lines = text.split('\n')
i = 0
length = len(lines)
for l in lines:
if len(l) > max_width:
cp = l
for ii in range(len(l)):
if ii % max_width == 0:
cp = cp[:ii] + '\n' + cp[ii:]
length += 1
lines[i] = cp
i += 1
text = '\n'.join(lines)
width = int(max_width * font_size * width_factor)
height = int(length * font_size * height_factor)
image = Image.new('RGB', (width, height), (255, 255, 255))
draw = ImageDraw.Draw(image)
text_font = ImageFont.truetype(font_path, font_size)
title_font = ImageFont.truetype(font_path, font_size + 5)
# 标题居中
title_width, title_height = title_font.getsize(title)
draw.text(((width - title_width) / 2, 10),
title, fill=(0, 0, 0), font=title_font)
# 文本不居中
draw.text((10, title_height+20), text, fill=(0, 0, 0), font=text_font)
return image
def render_markdown(markdown_text, image_width=800, image_height=600, font_size=26, font_color=(0, 0, 0), bg_color=(255, 255, 255)): def render_markdown(markdown_text, image_width=800, image_height=600, font_size=26, font_color=(0, 0, 0), bg_color=(255, 255, 255)):
HEADER_MARGIN = 20 HEADER_MARGIN = 20
@@ -370,41 +335,31 @@ def save_temp_img(img: Image) -> str:
logger.info(f"保存临时图片: {p}") logger.info(f"保存临时图片: {p}")
return p return p
async def download_image_by_url(url: str) -> str: async def download_image_by_url(url: str, post: bool = False, post_data: dict = None) -> str:
''' '''
下载图片 下载图片
''' '''
try: try:
logger.info(f"下载图片: {url}") logger.info(f"下载图片: {url}")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get(url) as resp: if post:
return save_temp_img(await resp.read()) async with session.post(url, json=post_data) as resp:
return save_temp_img(await resp.read())
else:
async with session.get(url) as resp:
return save_temp_img(await resp.read())
except aiohttp.client_exceptions.ClientConnectorSSLError as e: except aiohttp.client_exceptions.ClientConnectorSSLError as e:
# 关闭SSL验证 # 关闭SSL验证
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
async with aiohttp.ClientSession(trust_env=False) as session: async with aiohttp.ClientSession(trust_env=False) as session:
async with session.get(url, ssl=ssl_context) as resp: if post:
return save_temp_img(await resp.read()) async with session.get(url, ssl=ssl_context) as resp:
except Exception as e: return save_temp_img(await resp.read())
raise e else:
async with session.get(url, ssl=ssl_context) as resp:
return save_temp_img(await resp.read())
def create_text_image(title: str, text: str, max_width=30, font_size=20):
'''
文本转图片。
title: 标题
text: 文本内容
max_width: 文本宽度最大值(默认30)
font_size: 字体大小(默认20
返回:文件路径
'''
try:
img = word2img(title, text, max_width, font_size)
p = save_temp_img(img)
return p
except Exception as e: except Exception as e:
raise e raise e
@@ -435,21 +390,6 @@ def get_local_ip_addresses():
return ip return ip
def get_sys_info(global_object: GlobalObject):
mem = None
stats = global_object.dashboard_data.stats
os_name = platform.system()
os_version = platform.version()
if 'sys_perf' in stats and 'memory' in stats['sys_perf']:
mem = stats['sys_perf']['memory']
return {
'mem': mem,
'os': os_name + '_' + os_version,
'py': platform.python_version(),
}
def upload(_global_object: GlobalObject): def upload(_global_object: GlobalObject):
''' '''
上传相关非敏感统计数据 上传相关非敏感统计数据
@@ -512,7 +452,6 @@ def retry(n: int = 3):
return wrapper return wrapper
return decorator return decorator
def run_monitor(global_object: GlobalObject): def run_monitor(global_object: GlobalObject):
''' '''
监测机器性能 监测机器性能
+43
View File
@@ -0,0 +1,43 @@
import aiohttp, os
from util.general_utils import download_image_by_url, create_markdown_image
from type.config import VERSION
BASE_RENDER_URL = "https://t2i.soulter.top/text2img"
TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template")
async def text_to_image_base(text: str, return_url: bool = False) -> str:
'''
返回图像的文件路径
'''
with open(os.path.join(TEMPLATE_PATH, "base.html"), "r") as f:
tmpl_str = f.read()
assert(tmpl_str)
text = text.replace("`", "\`")
post_data = {
"tmpl": tmpl_str,
"json": return_url,
"tmpldata": {
"text": text,
"version": f"v{VERSION}",
},
"options": {
"full_page": True
}
}
if return_url:
async with aiohttp.ClientSession() as session:
async with session.post(f"{BASE_RENDER_URL}/generate", json=post_data) as resp:
ret = await resp.json()
return f"{BASE_RENDER_URL}/{ret['data']['id']}"
else:
image_path = ""
try:
image_path = await download_image_by_url(f"{BASE_RENDER_URL}/generate", post=True, post_data=post_data)
except Exception as e:
print(f"调用 markdown 渲染 API 失败,错误信息:{e},将使用本地渲染方式。")
image_path = create_markdown_image(text)
return image_path
+247
View File
@@ -0,0 +1,247 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
<link rel="stylesheet" href="/path/to/styles/default.min.css">
<script src="/path/to/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"
onload="renderMathInElement(document.getElementById('content'),{delimiters: [{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false}]});"></script>
</head>
<body>
<div style="background-color: #3276dc; color: #fff; font-size: 64px; ">
<span style="font-weight: bold; margin-left: 16px"># AstrBot</span>
<span>{{ version }}</span>
</div>
<article style="margin-top: 32px" id="content"></article>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
document.getElementById('content').innerHTML = marked.parse(`{{ text | safe}}`);
</script>
</body>
</html>
<style>
#content {
min-width: 200px;
max-width: 85%;
margin: 0 auto;
padding: 2rem 1em 1em;
}
body {
word-break: break-word;
line-height: 1.75;
font-weight: 400;
font-size: 32px;
margin: 0;
padding: 0;
overflow-x: hidden;
color: #333;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.5;
margin-top: 35px;
margin-bottom: 10px;
padding-bottom: 5px;
}
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
margin-top: -1.5rem;
margin-bottom: 1rem;
}
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
content: "#";
display: inline-block;
color: #3eaf7c;
padding-right: 0.23em;
}
h1 {
position: relative;
font-size: 2.5rem;
margin-bottom: 5px;
}
h1::before {
font-size: 2.5rem;
}
h2 {
padding-bottom: 0.5rem;
font-size: 2.2rem;
border-bottom: 1px solid #ececec;
}
h3 {
font-size: 1.5rem;
padding-bottom: 0;
}
h4 {
font-size: 1.25rem;
}
h5 {
font-size: 1rem;
}
h6 {
margin-top: 5px;
}
p {
line-height: inherit;
margin-top: 22px;
margin-bottom: 22px;
}
strong {
color: #3eaf7c;
}
img {
max-width: 100%;
border-radius: 2px;
display: block;
margin: auto;
border: 3px solid rgba(62, 175, 124, 0.2);
}
hr {
border-top: 1px solid #3eaf7c;
border-bottom: none;
border-left: none;
border-right: none;
margin-top: 32px;
margin-bottom: 32px;
}
code {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
word-break: break-word;
overflow-x: auto;
padding: 0.2rem 0.5rem;
margin: 0;
color: #3eaf7c;
font-size: 0.85em;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
pre {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
overflow: auto;
position: relative;
line-height: 1.75;
border-radius: 6px;
border: 2px solid #3eaf7c;
}
pre > code {
font-size: 12px;
padding: 15px 12px;
margin: 0;
word-break: normal;
display: block;
overflow-x: auto;
color: #333;
background: #f8f8f8;
}
a {
font-weight: 500;
text-decoration: none;
color: #3eaf7c;
}
a:hover, a:active {
border-bottom: 1.5px solid #3eaf7c;
}
a:before {
content: "⇲";
}
table {
display: inline-block !important;
font-size: 12px;
width: auto;
max-width: 100%;
overflow: auto;
border: solid 1px #3eaf7c;
}
thead {
background: #3eaf7c;
color: #fff;
text-align: left;
}
tr:nth-child(2n) {
background-color: rgba(62, 175, 124, 0.2);
}
th, td {
padding: 12px 7px;
line-height: 24px;
}
td {
min-width: 120px;
}
blockquote {
color: #666;
padding: 1px 23px;
margin: 22px 0;
border-left: 0.5rem solid rgba(62, 175, 124, 0.6);
border-color: #42b983;
background-color: #f8f8f8;
}
blockquote::after {
display: block;
content: "";
}
blockquote > p {
margin: 10px 0;
}
details {
border: none;
outline: none;
border-left: 4px solid #3eaf7c;
padding-left: 10px;
margin-left: 4px;
}
details summary {
cursor: pointer;
border: none;
outline: none;
background: white;
margin: 0px -17px;
}
details summary::-webkit-details-marker {
color: #3eaf7c;
}
ol, ul {
padding-left: 28px;
}
ol li, ul li {
margin-bottom: 0;
list-style: inherit;
}
ol li .task-list-item, ul li .task-list-item {
list-style: none;
}
ol li .task-list-item ul, ul li .task-list-item ul, ol li .task-list-item ol, ul li .task-list-item ol {
margin-top: 0;
}
ol ul, ul ul, ol ol, ul ol {
margin-top: 3px;
}
ol li {
padding-left: 6px;
}
ol li::marker {
color: #3eaf7c;
}
ul li {
list-style: none;
}
ul li:before {
content: "•";
margin-right: 4px;
color: #3eaf7c;
}
@media (max-width: 720px) {
h1 {
font-size: 24px;
}
h2 {
font-size: 20px;
}
h3 {
font-size: 18px;
}
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
from astrbot.core import oper_msg from astrbot.core import oper_msg
from type.message import AstrMessageEvent, AstrBotMessage from type.message import *
from type.command import CommandResult from type.command import CommandResult
from model.platform._message_result import MessageResult from model.platform._message_result import MessageResult
+29 -12
View File
@@ -16,6 +16,7 @@ from type.plugin import *
from type.register import * from type.register import *
from SparkleLogging.utils.core import LogManager from SparkleLogging.utils.core import LogManager
from logging import Logger from logging import Logger
from type.types import GlobalObject
logger: Logger = LogManager.GetLogger(log_name='astrbot-core') logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
@@ -91,7 +92,19 @@ def check_plugin_dept_update(cached_plugins: RegisteredPlugins, target_plugin: s
update_plugin_dept(os.path.join(plugin_path, "requirements.txt")) update_plugin_dept(os.path.join(plugin_path, "requirements.txt"))
def plugin_reload(cached_plugins: RegisteredPlugins): def has_init_param(cls, param_name):
try:
# 获取 __init__ 方法的签名
init_signature = inspect.signature(cls.__init__)
# 检查参数名是否在签名中
return param_name in init_signature.parameters
except (AttributeError, ValueError):
# 如果类没有 __init__ 方法或者无法获取签名
return False
def plugin_reload(ctx: GlobalObject):
cached_plugins = ctx.cached_plugins
plugins = get_plugin_modules() plugins = get_plugin_modules()
if plugins is None: if plugins is None:
return False, "未找到任何插件模块" return False, "未找到任何插件模块"
@@ -113,7 +126,12 @@ def plugin_reload(cached_plugins: RegisteredPlugins):
root_dir_name + "." + p, fromlist=[p]) root_dir_name + "." + p, fromlist=[p])
cls = get_classes(p, module) cls = get_classes(p, module)
obj = getattr(module, cls[0])()
try:
# 尝试传入 ctx
obj = getattr(module, cls[0])(ctx=ctx)
except:
obj = getattr(module, cls[0])()
metadata = None metadata = None
try: try:
@@ -125,8 +143,7 @@ def plugin_reload(cached_plugins: RegisteredPlugins):
else: else:
metadata = PluginMetadata( metadata = PluginMetadata(
plugin_name=info['name'], plugin_name=info['name'],
plugin_type=PluginType.COMMON if 'plugin_type' not in info else PluginType( plugin_type=PluginType.COMMON if 'plugin_type' not in info else PluginType(info['plugin_type']),
info['plugin_type']),
author=info['author'], author=info['author'],
desc=info['desc'], desc=info['desc'],
version=info['version'], version=info['version'],
@@ -163,7 +180,7 @@ def update_plugin_dept(path):
os.system(f"{py} -m pip install -r {path} -i {mirror} --quiet") os.system(f"{py} -m pip install -r {path} -i {mirror} --quiet")
def install_plugin(repo_url: str, cached_plugins: RegisteredPlugins): def install_plugin(repo_url: str, ctx: GlobalObject):
ppath = get_plugin_store_path() ppath = get_plugin_store_path()
# 删除末尾的 / # 删除末尾的 /
if repo_url.endswith("/"): if repo_url.endswith("/"):
@@ -178,7 +195,7 @@ def install_plugin(repo_url: str, cached_plugins: RegisteredPlugins):
if os.path.exists(plugin_path): if os.path.exists(plugin_path):
remove_dir(plugin_path) remove_dir(plugin_path)
Repo.clone_from(repo_url, to_path=plugin_path, branch='master') Repo.clone_from(repo_url, to_path=plugin_path, branch='master')
ok, err = plugin_reload(cached_plugins) ok, err = plugin_reload(ctx)
if not ok: if not ok:
raise Exception(err) raise Exception(err)
@@ -192,19 +209,19 @@ def get_registered_plugin(plugin_name: str, cached_plugins: RegisteredPlugins) -
return ret return ret
def uninstall_plugin(plugin_name: str, cached_plugins: RegisteredPlugins): def uninstall_plugin(plugin_name: str, ctx: GlobalObject):
plugin = get_registered_plugin(plugin_name, cached_plugins) plugin = get_registered_plugin(plugin_name, ctx.cached_plugins)
if not plugin: if not plugin:
raise Exception("插件不存在。") raise Exception("插件不存在。")
root_dir_name = plugin.root_dir_name root_dir_name = plugin.root_dir_name
ppath = get_plugin_store_path() ppath = get_plugin_store_path()
cached_plugins.remove(plugin) ctx.cached_plugins.remove(plugin)
if not remove_dir(os.path.join(ppath, root_dir_name)): if not remove_dir(os.path.join(ppath, root_dir_name)):
raise Exception("移除插件成功,但是删除插件文件夹失败。您可以手动删除该文件夹,位于 addons/plugins/ 下。") raise Exception("移除插件成功,但是删除插件文件夹失败。您可以手动删除该文件夹,位于 addons/plugins/ 下。")
def update_plugin(plugin_name: str, cached_plugins: RegisteredPlugins): def update_plugin(plugin_name: str, ctx: GlobalObject):
plugin = get_registered_plugin(plugin_name, cached_plugins) plugin = get_registered_plugin(plugin_name, ctx.cached_plugins)
if not plugin: if not plugin:
raise Exception("插件不存在。") raise Exception("插件不存在。")
ppath = get_plugin_store_path() ppath = get_plugin_store_path()
@@ -212,7 +229,7 @@ def update_plugin(plugin_name: str, cached_plugins: RegisteredPlugins):
plugin_path = os.path.join(ppath, root_dir_name) plugin_path = os.path.join(ppath, root_dir_name)
repo = Repo(path=plugin_path) repo = Repo(path=plugin_path)
repo.remotes.origin.pull() repo.remotes.origin.pull()
ok, err = plugin_reload(cached_plugins) ok, err = plugin_reload(ctx)
if not ok: if not ok:
raise Exception(err) raise Exception(err)
+33 -6
View File
@@ -6,10 +6,35 @@ except BaseException as e:
has_git = False has_git = False
import sys, os import sys, os
import requests import requests
import psutil
from type.config import VERSION from type.config import VERSION
from SparkleLogging.utils.core import LogManager
from logging import Logger
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
def terminate_child_processes():
try:
parent = psutil.Process(os.getpid())
children = parent.children(recursive=True)
logger.info(f"正在终止 {len(children)} 个子进程。")
for child in children:
logger.info(f"正在终止子进程 {child.pid}")
child.terminate()
try:
child.wait(timeout=3)
except psutil.NoSuchProcess:
continue
except psutil.TimeoutExpired:
logger.info(f"子进程 {child.pid} 没有被正常终止, 正在强行杀死。")
child.kill()
except psutil.NoSuchProcess:
pass
def _reboot(): def _reboot():
py = sys.executable py = sys.executable
terminate_child_processes()
os.execl(py, py, *sys.argv) os.execl(py, py, *sys.argv)
def find_repo() -> Repo: def find_repo() -> Repo:
@@ -81,18 +106,20 @@ def check_update() -> str:
if curr_commit.startswith(new_commit): if curr_commit.startswith(new_commit):
return f"当前已经是最新版本: v{VERSION}" return f"当前已经是最新版本: v{VERSION}"
else: else:
update_info = f"""有新版本可用。 update_info = f"""> 有新版本可用,请及时更新
=== 当前版本 === # 当前版本
v{VERSION} v{VERSION}
=== 新版本 === # 最新版本
{update_data[0]['version']} {update_data[0]['version']}
=== 发布时间 === # 发布时间
{update_data[0]['published_at']} {update_data[0]['published_at']}
=== 更新内容 === # 更新内容
{update_data[0]['body']}""" ---
{update_data[0]['body']}
---"""
return update_info return update_info
def update_project(update_data: list, def update_project(update_data: list,