Compare commits

...

26 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
Soulter e8773cea7f fix: 修复配置文件没有有效迁移的问题 2024-05-25 20:59:37 +08:00
Soulter 4d36ffcb08 fix: 优化插件的结果处理 2024-05-25 18:46:38 +08:00
Soulter c653e492c4 Merge pull request #164 from Soulter/stat-upload-perf
/models 指令优化
2024-05-25 18:35:56 +08:00
Soulter f08de1f404 perf: 添加 models 指令到帮助中 2024-05-25 18:34:08 +08:00
Soulter 1218691b61 perf: model 指令放宽限制,支持输入自定义模型。设置模型后持久化保存。 2024-05-25 18:29:01 +08:00
Soulter 61fc27ff79 Merge pull request #163 from Soulter/stat-upload-perf
优化统计记录数据结构
2024-05-25 18:28:08 +08:00
Soulter 123ee24f7e fix: stat perf 2024-05-25 18:01:16 +08:00
Soulter 52c9045a28 feat: 优化了统计信息数据结构 2024-05-25 17:47:41 +08:00
Soulter f00f1e8933 fix: 画图报错 2024-05-24 13:33:02 +08:00
Soulter 8da4433e57 chore: 更改相关字段 2024-05-21 08:44:05 +08:00
Soulter 7babb87934 perf: 更改库的加载顺序 2024-05-21 08:41:46 +08:00
25 changed files with 672 additions and 338 deletions
+15 -38
View File
@@ -7,62 +7,37 @@
# AstrBot
[![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">
<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">
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-322154837-purple">
</a>
<img alt="Static Badge" src="https://img.shields.io/badge/频道-x42d56aki2-purple">
<a href="https://astrbot.soulter.top/center">项目部署</a>
<a href="https://github.com/Soulter/QQChannelChatGPT/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://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">插件开发</a>
</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. 可视化面板
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 官方机器人接口
🌍支持的消息平台
- QQ 群、QQ 频道(OneBot、QQ 官方接口)
- Telegram(由 [astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持)
🌍支持的AI语言模型一览:
🌍支持的模型一览:
**文字模型/图片理解**
- OpenAI GPT-3(原生支持)
- OpenAI GPT-3.5(原生支持)
- OpenAI GPT-4(原生支持)
- OpenAI GPT、DallE 系列
- Claude(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
- HuggingChat(免费,由[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 大语言模型接入。
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等
- `sysstat`: https://github.com/Soulter/sysstatqcbot | 查看系统状态
@@ -141,6 +116,8 @@
- `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">
+2 -2
View File
@@ -119,14 +119,14 @@ class DashBoardHelper():
)
qq_gocq_platform_group = DashBoardConfig(
config_type="group",
name="OneBot协议平台配置",
name="go-cqhttp",
description="",
body=[
DashBoardConfig(
config_type="item",
val_type="bool",
name="启用",
description="支持cq-http、shamrock等(目前仅支持QQ平台)",
description="",
value=config['gocqbot']['enable'],
path="gocqbot.enable",
),
+7 -8
View File
@@ -16,7 +16,7 @@ from persist.session import dbConn
from type.register import RegisteredPlugin
from typing import List
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 logging import Logger
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
@@ -196,7 +196,7 @@ class AstrBotDashBoard():
repo_url = post_data["url"]
try:
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} 成功")
return Response(
status="success",
@@ -237,7 +237,7 @@ class AstrBotDashBoard():
plugin_name = post_data["name"]
try:
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} 成功")
return Response(
status="success",
@@ -344,8 +344,7 @@ class AstrBotDashBoard():
def shutdown_bot(self, delay_s: int):
time.sleep(delay_s)
py = sys.executable
os.execl(py, py, *sys.argv)
_reboot()
def _get_configs(self, namespace: str):
if namespace == "":
@@ -390,13 +389,13 @@ class AstrBotDashBoard():
},
{
"title": "QQ_OFFICIAL",
"desc": "QQ官方API,仅支持频道",
"desc": "QQ官方API支持频道、群(需获得群权限)",
"namespace": "internal_platform_qq_official",
"tag": ""
},
{
"title": "OneBot协议",
"desc": "支持cq-http、shamrock等(目前仅支持QQ平台)",
"title": "go-cqhttp",
"desc": "第三方 QQ 协议实现。支持频道、群",
"namespace": "internal_platform_qq_gocq",
"tag": ""
}
+9 -26
View File
@@ -22,6 +22,7 @@ from util.cmd_config import init_astrbot_config_items
from type.types import GlobalObject
from type.register import *
from type.message import AstrBotMessage
from type.config import *
from addons.dashboard.helper import DashBoardHelper
from addons.dashboard.server import DashBoardData
from persist.session import dbConn
@@ -38,9 +39,6 @@ frequency_time = 60
# 计数默认值
frequency_count = 10
# 版本
version = '3.1.13'
# 语言模型
OPENAI_OFFICIAL = 'openai_official'
NONE_LLM = 'none_llm'
@@ -56,13 +54,9 @@ baidu_judge = None
# CLI
PLATFORM_CLI = 'cli'
init_astrbot_config_items()
# 全局对象
_global_object: GlobalObject = None
# 语言模型选择
def privider_chooser(cfg):
l = []
@@ -70,21 +64,16 @@ def privider_chooser(cfg):
l.append('openai_official')
return l
'''
初始化机器人
'''
def init():
'''
初始化机器人
'''
global llm_instance, llm_command_instance
global baidu_judge, chosen_provider
global frequency_count, frequency_time
global _global_object
# 迁移旧配置
gu.try_migrate_config()
# 使用新配置
init_astrbot_config_items()
cfg = cc.get_all()
_event_loop = asyncio.new_event_loop()
@@ -92,9 +81,10 @@ def init():
# 初始化 global_object
_global_object = GlobalObject()
_global_object.version = version
_global_object.version = VERSION
_global_object.base_config = cfg
logger.info("AstrBot v"+version)
_global_object.logger = logger
logger.info("AstrBot v" + VERSION)
if 'reply_prefix' in cfg:
# 适配旧版配置
@@ -182,7 +172,7 @@ def init():
logger.info("正在载入插件...")
# 加载插件
_command = Command(None, _global_object)
ok, err = putil.plugin_reload(_global_object.cached_plugins)
ok, err = putil.plugin_reload(_global_object)
if ok:
logger.info(
f"成功载入 {len(_global_object.cached_plugins)} 个插件")
@@ -319,7 +309,6 @@ async def record_message(platform: str, session_id: str):
db_inst.increment_stat_session(platform, session_id, 1)
db_inst.increment_stat_message(curr_ts, 1)
db_inst.increment_stat_platform(curr_ts, platform, 1)
_global_object.cnt_total += 1
async def oper_msg(message: AstrBotMessage,
@@ -453,12 +442,6 @@ async def oper_msg(message: AstrBotMessage,
return
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]:
return MessageResult(f"指令调用错误: \n{str(command_result[1])}")
+18 -19
View File
@@ -4,14 +4,12 @@ import sys
import warnings
import traceback
import threading
from SparkleLogging.utils.core import LogManager
from logging import Formatter, Logger
from util.cmd_config import CmdConfig, try_migrate_config
warnings.filterwarnings("ignore")
abs_path = os.path.dirname(os.path.realpath(sys.argv[0])) + '/'
logger: Logger = None
logo_tmpl = """
___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | |
@@ -35,9 +33,11 @@ def update_dept():
'''
# 获取 Python 可执行文件路径
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/"
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():
try:
@@ -65,8 +65,6 @@ def main():
input("未知错误。")
exit()
make_necessary_dirs()
# 启动主程序(cores/qqbot/core.py
bot_core.init()
@@ -78,26 +76,27 @@ def check_env():
exit()
if __name__ == "__main__":
update_dept()
make_necessary_dirs()
try_migrate_config()
cc = CmdConfig()
http_proxy = cc.get("http_proxy")
https_proxy = cc.get("https_proxy")
if http_proxy:
os.environ['HTTP_PROXY'] = http_proxy
if https_proxy:
os.environ['HTTPS_PROXY'] = https_proxy
os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com'
from SparkleLogging.utils.core import LogManager
logger = LogManager.GetLogger(
log_name='astrbot-core',
out_to_console=True,
custom_formatter=Formatter('[%(asctime)s| %(name)s - %(levelname)s|%(filename)s:%(lineno)d]: %(message)s', datefmt="%H:%M:%S")
)
logger.info(logo_tmpl)
# 设置代理
from util.cmd_config import CmdConfig
cc = CmdConfig()
http_proxy = cc.get("http_proxy")
https_proxy = cc.get("https_proxy")
logger.info(f"使用代理: {http_proxy}, {https_proxy}")
if http_proxy:
os.environ['HTTP_PROXY'] = http_proxy
if https_proxy:
os.environ['HTTPS_PROXY'] = https_proxy
os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com'
update_dept()
check_env()
t = threading.Thread(target=main, daemon=True)
t.start()
+44 -43
View File
@@ -11,6 +11,7 @@ from nakuru.entities.components import (
Image
)
from util import general_utils as gu
from util.image_render.helper import text_to_image_base
from model.provider.provider import Provider
from util.cmd_config import CmdConfig as cc
from type.message import *
@@ -64,6 +65,8 @@ class Command:
result = await plugin.plugin_instance.run(ame)
else:
result = await asyncio.to_thread(plugin.plugin_instance.run, ame)
if not result:
continue
if isinstance(result, CommandResult):
hit = result.hit
res = result._result_tuple()
@@ -73,6 +76,7 @@ class Command:
else:
raise TypeError("插件返回值格式错误。")
if hit:
plugin.trig()
logger.debug("hit plugin: " + plugin.metadata.plugin_name)
return True, res
except TypeError as e:
@@ -94,14 +98,16 @@ class Command:
if self.command_start_with(message, "nick"):
return True, self.set_nick(message, platform, role)
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"):
return True, self.get_my_id(message_obj, platform)
if self.command_start_with(message, "web"): # 网页搜索
return True, self.web_search(message)
if self.command_start_with(message, "update"):
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 False, None
@@ -116,40 +122,34 @@ class Command:
elif l[1] == 'off':
self.global_object.web_search = False
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):
try:
user_id = str(message_obj.user_id)
user_id = str(message_obj.sender.user_id)
return True, f"你在此平台上的ID{user_id}", "plugin"
except BaseException as e:
return False, f"{platform}上获取你的ID失败,原因: {str(e)}", "plugin"
def get_new_conf(self, message, role):
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):
async def plugin_oper(self, message: str, role: str, ctx: GlobalObject, platform: str):
l = message.split(" ")
if len(l) < 2:
p = gu.create_text_image(
"【插件指令面板】", "安装插件: \nplugin i 插件Github地址\n卸载插件: \nplugin d 插件名 \n重载插件: \nplugin reload\n查看插件列表:\nplugin l\n更新插件: plugin u 插件名\n")
return True, [Image.fromFileSystem(p)], "plugin"
p = await text_to_image_base("# 插件指令面板 \n- 安装插件: `plugin i 插件Github地址`\n- 卸载插件: `plugin d 插件名`\n- 重载插件: `plugin reload`\n- 查看插件列表:`plugin l`\n - 更新插件: `plugin u 插件名`\n")
with open(p, 'rb') as f:
return True, [Image.fromBytes(f.read())], "plugin"
else:
if l[1] == "i":
if role != "admin":
return False, f"你的身份组{role}没有权限安装插件", "plugin"
try:
putil.install_plugin(l[2], cached_plugins)
putil.install_plugin(l[2], )
return True, "插件拉取并载入成功~", "plugin"
except BaseException as e:
return False, f"拉取插件失败,原因: {str(e)}", "plugin"
@@ -157,37 +157,37 @@ class Command:
if role != "admin":
return False, f"你的身份组{role}没有权限删除插件", "plugin"
try:
putil.uninstall_plugin(l[2], cached_plugins)
putil.uninstall_plugin(l[2], ctx)
return True, "插件卸载成功~", "plugin"
except BaseException as e:
return False, f"卸载插件失败,原因: {str(e)}", "plugin"
elif l[1] == "u":
try:
putil.update_plugin(l[2], cached_plugins)
putil.update_plugin(l[2], ctx)
return True, "\n更新插件成功!!", "plugin"
except BaseException as e:
return False, f"更新插件失败,原因: {str(e)}\n建议: 使用 plugin i 指令进行覆盖安装(插件数据可能会丢失)", "plugin"
elif l[1] == "l":
try:
plugin_list_info = ""
for plugin in 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"
p = gu.create_text_image(
"【已激活插件列表】", plugin_list_info + "\n使用plugin v 插件名 查看插件帮助\n")
return True, [Image.fromFileSystem(p)], "plugin"
for plugin in ctx.cached_plugins:
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 = await text_to_image_base(f"# 已激活的插件\n{plugin_list_info}\n> 使用plugin v 插件名 查看插件帮助\n")
with open(p, 'rb') as f:
return True, [Image.fromBytes(f.read())], "plugin"
except BaseException as e:
return False, f"获取插件列表失败,原因: {str(e)}", "plugin"
elif l[1] == "v":
try:
info = None
for i in cached_plugins:
for i in ctx.cached_plugins:
if i.metadata.plugin_name == l[2]:
info = i.metadata
break
if info:
p = gu.create_text_image(
f"【插件信息】", f"名称: {info.plugin_name}\n类型: {info.plugin_type}\n{info.desc}\n版本: {info.version}\n作者: {info.author}")
return True, [Image.fromFileSystem(p)], "plugin"
p = await text_to_image_base(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.fromBytes(f.read())], "plugin"
else:
return False, "未找到该插件", "plugin"
except BaseException as e:
@@ -217,7 +217,7 @@ class Command:
"help": "帮助",
"keyword": "设置关键词/关键指令回复",
"update": "更新项目",
"nick": "设置机器人昵称",
"nick": "设置机器人唤醒词",
"plugin": "插件安装、卸载和重载",
"web on/off": "LLM 网页搜索能力",
}
@@ -229,22 +229,23 @@ class Command:
notice = (await resp.json())["notice"]
except BaseException as e:
notice = ""
msg = "# Help Center\n## 指令列表\n"
msg = "## 指令列表\n"
for key, value in commands.items():
msg += f"`{key}` - {value}\n"
msg += f"- `{key}`: {value}\n"
# plugins
if cached_plugins != None:
if cached_plugins:
plugin_list_info = ""
for plugin in cached_plugins:
plugin_list_info += f"`{plugin.metadata.plugin_name}` {plugin.metadata.desc}\n"
if plugin_list_info.strip() != "":
msg += "\n## 插件列表\n> 使用plugin v 插件名 查看插件帮助\n"
plugin_list_info += f"- `{plugin.metadata.plugin_name}`: {plugin.metadata.desc}\n"
if plugin_list_info.strip():
msg += "\n## 插件列表\n> 使用 plugin v 插件名 查看插件帮助\n"
msg += plugin_list_info
msg += notice
try:
p = gu.create_markdown_image(msg)
return [Image.fromFileSystem(p),]
p = await text_to_image_base(msg)
with open(p, 'rb') as f:
return [Image.fromBytes(f.read()),]
except BaseException as e:
logger.error(str(e))
return msg
@@ -265,7 +266,7 @@ class Command:
if len(l) == 1:
try:
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"
except BaseException as e:
return False, "检查更新失败: "+str(e), "update"
+18 -23
View File
@@ -84,6 +84,7 @@ class CommandOpenAIOfficial(Command):
for model in models:
ret += f"\n{i}. {model.id}"
i += 1
ret += "\nTips: 使用 /model 模型名/编号,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
logger.debug(ret)
return True, ret, "models"
@@ -93,31 +94,28 @@ class CommandOpenAIOfficial(Command):
if len(l) == 1:
return True, "请输入 /model 模型名/编号", "model"
model = str(l[1])
models = await self.get_models()
models = list(models)
if model.isdigit() and int(model) <= len(models) and int(model) >= 1:
model = models[int(model)-1]
if model.isdigit():
models = await self.get_models()
models = list(models)
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:
f = False
for m in models:
if model == m.id:
f = True
break
if not f:
return True, "模型不存在或输入非法", "model"
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):
commands = super().general_commands()
commands[''] = '调用 OpenAI DallE 模型生成图片'
commands['set'] = '人格设置面板'
commands['status'] = '查看 Api Key 状态和配置信息'
commands['token'] = '查看本轮会话 token'
commands['reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt'
commands['reset p'] = '重置当前与 LLM 的会话,并清除人格。'
commands['/set'] = '人格设置面板'
commands['/status'] = '查看 Api Key 状态和配置信息'
commands['/token'] = '查看本轮会话 token'
commands['/reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt'
commands['/reset p'] = '重置当前与 LLM 的会话,并清除人格。'
commands['/models'] = '获取当前可用的模型'
commands['/model'] = '更换模型'
return True, await super().help_messager(commands, self.platform, self.global_object.cached_plugins), "help"
@@ -248,9 +246,6 @@ class CommandOpenAIOfficial(Command):
async def draw(self, message: str):
if self.provider is None:
return False, "未启用 OpenAI 官方 API", "draw"
if message.startswith("/"):
message = message[2:]
elif message.startswith(""):
message = message[1:]
message = message.removeprefix("/").removeprefix("")
img_url = await self.provider.image_generate(message)
return True, img_url, "draw"
+8 -4
View File
@@ -7,7 +7,8 @@ from nakuru import (
import botpy.message
from type.message import *
from typing import List, Union
import time
from util.general_utils import save_temp_img
import time, base64
# QQ官方消息类型转换
@@ -18,11 +19,14 @@ def qq_official_message_parse(message: List[BaseMessageComponent]):
for i in message:
if isinstance(i, Plain):
plain_text += i.text
elif isinstance(i, Image) and image_path == None:
if i.path is not None:
elif isinstance(i, Image) and not image_path:
if 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:
image_path = i.file
image_path = save_temp_img(i.file)
return plain_text, image_path
# QQ官方消息类型 2 AstrBotMessage
+10 -4
View File
@@ -14,34 +14,40 @@ class Platform():
初始化平台的各种接口
'''
self.message_handler = message_handler
self.cnt_receive = 0
self.cnt_reply = 0
pass
@abc.abstractmethod
async def handle_msg():
async def handle_msg(self):
'''
处理到来的消息
'''
self.cnt_receive += 1
pass
@abc.abstractmethod
async def reply_msg():
async def reply_msg(self):
'''
回复消息(被动发送)
'''
self.cnt_reply += 1
pass
@abc.abstractmethod
async def send_msg(target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
async def send_msg(self, target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
'''
发送消息(主动发送)
'''
self.cnt_reply += 1
pass
@abc.abstractmethod
async def send(target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
async def send(self, target: Union[GuildMessage, GroupMessage, FriendMessage, str], message: Union[str, list]):
'''
发送消息(主动发送)同 send_msg()
'''
self.cnt_reply += 1
pass
def parse_message_outline(self, message: Union[GuildMessage, GroupMessage, FriendMessage, str, list]) -> str:
+15 -20
View File
@@ -1,5 +1,6 @@
from nakuru.entities.components import Plain, At, Image, Node
from util import general_utils as gu
from util.image_render.helper import text_to_image_base
from util.cmd_config import CmdConfig
import asyncio
from nakuru import (
@@ -11,6 +12,7 @@ from nakuru import (
Notify
)
from typing import Union
from type.types import GlobalObject
import time
from ._platfrom import Platform
@@ -29,7 +31,7 @@ class FakeSource:
class QQGOCQ(Platform):
def __init__(self, cfg: dict, message_handler: callable, global_object) -> None:
def __init__(self, cfg: dict, message_handler: callable, global_object: GlobalObject) -> None:
super().__init__(message_handler)
self.loop = asyncio.new_event_loop()
@@ -38,14 +40,8 @@ class QQGOCQ(Platform):
self.waiting = {}
self.cc = CmdConfig()
self.cfg = cfg
try:
self.nick_qq = cfg['nick_qq']
except:
self.nick_qq = ["ai", "!", ""]
nick_qq = self.nick_qq
if isinstance(nick_qq, str):
nick_qq = [nick_qq]
self.context = global_object
self.unique_session = cfg['uniqueSessionMode']
self.pic_mode = cfg['qq_pic_mode']
@@ -109,6 +105,7 @@ class QQGOCQ(Platform):
self.client.run()
async def handle_msg(self, message: AstrBotMessage):
await super().handle_msg()
logger.info(
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
@@ -132,8 +129,8 @@ class QQGOCQ(Platform):
if message.type.value == "GroupMessage":
if str(i.qq) == str(message.self_id):
resp = True
elif isinstance(i, Plain):
for nick in self.nick_qq:
elif isinstance(i, Plain) and self.context.nick:
for nick in self.context.nick:
if nick != '' and i.text.strip().startswith(nick):
resp = True
break
@@ -181,6 +178,7 @@ class QQGOCQ(Platform):
async def reply_msg(self,
message: Union[AstrBotMessage, GuildMessage, GroupMessage, FriendMessage],
result_message: list):
await super().reply_msg()
"""
插件开发者请使用send方法, 可以不用直接调用这个方法。
"""
@@ -216,7 +214,8 @@ class QQGOCQ(Platform):
news.append(i)
plains_str = "".join(plains).strip()
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))
res = news
@@ -259,6 +258,7 @@ class QQGOCQ(Platform):
提供给插件的发送QQ消息接口。
参数说明:第一个参数可以是消息对象,也可以是QQ群号。第二个参数是消息内容(消息内容可以是消息链列表,也可以是纯文字信息)。
'''
await super().reply_msg()
try:
await self.reply_msg(message, result_message)
except BaseException as e:
@@ -270,22 +270,17 @@ class QQGOCQ(Platform):
'''
同 send_msg()
'''
await super().reply_msg()
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: 文本内容
max_width: 文本宽度最大值(默认30)
font_size: 字体大小(默认20
返回:文件路径
'''
try:
img = gu.word2img(title, text, max_width, font_size)
p = gu.save_temp_img(img)
return p
return await text_to_image_base(text)
except Exception as e:
raise e
+51 -25
View File
@@ -19,7 +19,8 @@ from ._message_parse import (
)
from type.message import *
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 logging import Logger
@@ -64,6 +65,7 @@ class QQOfficial(Platform):
self.secret = cfg['qqbot_secret']
self.unique_session = cfg['uniqueSessionMode']
qq_group = cfg['qqofficial_enable_group_message']
self.pic_mode = cfg['qq_pic_mode']
if qq_group:
self.intents = botpy.Intents(
@@ -102,6 +104,7 @@ class QQOfficial(Platform):
)
async def handle_msg(self, message: AstrBotMessage):
await super().handle_msg()
assert isinstance(message.raw_message, (botpy.message.Message,
botpy.message.GroupMessage, botpy.message.DirectMessage))
is_group = message.type != MessageType.FRIEND_MESSAGE
@@ -154,6 +157,7 @@ class QQOfficial(Platform):
'''
回复频道消息
'''
await super().reply_msg()
if isinstance(message, AstrBotMessage):
source = message.raw_message
else:
@@ -167,32 +171,54 @@ class QQOfficial(Platform):
image_path = ''
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):
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:
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)
plain_text = res
if source is not None and image_path == '': # file_image与message_reference不能同时传入
msg_ref = Reference(message_id=source.id,
@@ -213,7 +239,7 @@ class QQOfficial(Platform):
data['guild_id'] = source.guild_id
else:
raise ValueError(f"未知的消息类型: {message.type}")
if image_path != '':
if image_path:
data['file_image'] = image_path
try:
+6 -1
View File
@@ -73,6 +73,7 @@ class ProviderOpenAIOfficial(Provider):
base_url=self.base_url
)
self.model_configs: Dict = cfg['chatGPTConfigs']
super().set_curr_model(self.model_configs['model'])
self.image_generator_model_configs: Dict = self.cc.get('openai_image_generate', None)
self.session_memory: Dict[str, List] = {} # 会话记忆
self.session_memory_lock = threading.Lock()
@@ -289,6 +290,7 @@ class ProviderOpenAIOfficial(Provider):
extra_conf: Dict = None,
**kwargs
) -> str:
super().accu_model_stat()
if not session_id:
session_id = "unknown"
if "unknown" in self.session_memory:
@@ -415,12 +417,13 @@ class ProviderOpenAIOfficial(Provider):
return False
async def image_generate(self, prompt, session_id, **kwargs) -> str:
async def image_generate(self, prompt: str, session_id: str = None, **kwargs) -> str:
'''
生成图片
'''
retry = 0
conf = self.image_generator_model_configs
super().accu_model_stat(model=conf['model'])
if not conf:
logger.error("OpenAI 图片生成模型配置不存在。")
raise Exception("OpenAI 图片生成模型配置不存在。")
@@ -481,6 +484,8 @@ class ProviderOpenAIOfficial(Provider):
def set_model(self, model: str):
self.model_configs['model'] = model
self.cc.put_by_dot_str("openai.chatGPTConfigs.model", model)
super().set_curr_model(model)
def get_configs(self):
return self.model_configs
+26 -3
View File
@@ -1,4 +1,27 @@
from collections import defaultdict
class Provider:
def __init__(self) -> None:
self.model_stat = defaultdict(int) # 用于记录 LLM Model 使用数据
self.curr_model_name = "unknown"
def reset_model_stat(self):
self.model_stat.clear()
def set_curr_model(self, model_name: str):
self.curr_model_name = model_name
def get_curr_model(self):
'''
返回当前正在使用的 LLM
'''
return self.curr_model_name
def accu_model_stat(self, model: str = None):
if not model:
model = self.get_curr_model()
self.model_stat[model] += 1
async def text_chat(self,
prompt: str,
session_id: str,
@@ -18,7 +41,7 @@ class Provider:
extra_conf: 额外配置
default_personality: 默认人格
'''
raise NotImplementedError
raise NotImplementedError()
async def image_generate(self, prompt, session_id, **kwargs) -> str:
'''
@@ -26,10 +49,10 @@ class Provider:
prompt: 提示词
session_id: 会话id
'''
raise NotImplementedError
raise NotImplementedError()
async def forget(self, session_id=None) -> bool:
'''
重置会话
'''
raise NotImplementedError
raise NotImplementedError()
+1 -1
View File
@@ -18,7 +18,7 @@ class CommandResult():
用于在Command中返回多个值
'''
def __init__(self, hit: bool, success: bool, message_chain: list, command_name: str = "unknown_command") -> None:
def __init__(self, hit: bool, success: bool = False, message_chain: list = [], command_name: str = "unknown_command") -> None:
self.hit = hit
self.success = success
self.message_chain = message_chain
+1
View File
@@ -0,0 +1 @@
VERSION = '3.2.4'
+1 -1
View File
@@ -2,7 +2,7 @@ from enum import Enum
from dataclasses import dataclass
class PluginType(Enum):
PLATFORM = 'platfrom' # 平台类插件。
PLATFORM = 'platform' # 平台类插件。
LLM = 'llm' # 大语言模型类插件
COMMON = 'common' # 其他插件
+7
View File
@@ -15,6 +15,13 @@ class RegisteredPlugin:
module_path: str
module: ModuleType
root_dir_name: str
trig_cnt: int = 0
def reset_trig_cnt(self):
self.trig_cnt = 0
def trig(self):
self.trig_cnt += 1
def __str__(self) -> str:
return f"RegisteredPlugin({self.metadata}, {self.module_path}, {self.root_dir_name})"
+4 -2
View File
@@ -1,5 +1,7 @@
from type.register import *
from typing import List
from logging import Logger
class GlobalObject:
'''
@@ -15,9 +17,10 @@ class GlobalObject:
web_search: bool # 是否开启了网页搜索
reply_prefix: str # 回复前缀
unique_session: bool # 是否开启了独立会话
cnt_total: int # 总消息数
default_personality: dict
dashboard_data = None
logger: Logger = None
def __init__(self):
self.nick = None # gocq 的昵称
@@ -26,7 +29,6 @@ class GlobalObject:
self.web_search = False # 是否开启了网页搜索
self.reply_prefix = None
self.unique_session = False
self.cnt_total = 0
self.platforms = []
self.llms = []
self.default_personality = None
+26
View File
@@ -1,5 +1,6 @@
import os
import json
import yaml
from typing import Union
cpath = "data/cmd_config.json"
@@ -117,3 +118,28 @@ def init_astrbot_config_items():
cc.init_attributes("https_proxy", "")
cc.init_attributes("dashboard_username", "")
cc.init_attributes("dashboard_password", "")
def try_migrate_config():
'''
将 cmd_config.json 迁移至 data/cmd_config.json
'''
print("try migrate configs")
if os.path.exists("cmd_config.json"):
with open("cmd_config.json", "r", encoding="utf-8-sig") as f:
data = json.load(f)
with open("data/cmd_config.json", "w", encoding="utf-8-sig") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
try:
os.remove("cmd_config.json")
except Exception as e:
pass
if not os.path.exists("cmd_config.json") and not os.path.exists("data/cmd_config.json"):
# 从 configs/config.yaml 上拿数据
configs_pth = os.path.abspath(os.path.join(os.path.abspath(__file__), "../../configs/config.yaml"))
with open(configs_pth, encoding='utf-8') as f:
data = yaml.load(f, Loader=yaml.Loader)
print(data)
with open("data/cmd_config.json", "w", encoding="utf-8-sig") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
+47 -97
View File
@@ -10,6 +10,7 @@ import json
import sys
import psutil
import ssl
import base64
from PIL import Image, ImageDraw, ImageFont
from type.types import GlobalObject
@@ -46,42 +47,6 @@ def get_font_path() -> str:
raise Exception("找不到字体文件")
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)):
HEADER_MARGIN = 20
@@ -370,41 +335,31 @@ def save_temp_img(img: Image) -> str:
logger.info(f"保存临时图片: {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:
logger.info(f"下载图片: {url}")
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return save_temp_img(await resp.read())
if post:
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:
# 关闭SSL验证
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
async with aiohttp.ClientSession(trust_env=False) as session:
async with session.get(url, ssl=ssl_context) as resp:
return save_temp_img(await resp.read())
except Exception as e:
raise e
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
if post:
async with session.get(url, ssl=ssl_context) as resp:
return save_temp_img(await resp.read())
else:
async with session.get(url, ssl=ssl_context) as resp:
return save_temp_img(await resp.read())
except Exception as e:
raise e
@@ -422,21 +377,6 @@ def create_markdown_image(text: str):
raise e
def try_migrate_config():
'''
将 cmd_config.json 迁移至 data/cmd_config.json
'''
if os.path.exists("cmd_config.json"):
with open("cmd_config.json", "r", encoding="utf-8-sig") as f:
data = json.load(f)
with open("data/cmd_config.json", "w", encoding="utf-8-sig") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
try:
os.remove("cmd_config.json")
except Exception as e:
pass
def get_local_ip_addresses():
ip = ''
try:
@@ -450,31 +390,42 @@ def get_local_ip_addresses():
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):
'''
上传相关非敏感统计数据
'''
time.sleep(10)
while True:
addr_ip = ''
platform_stats = {}
llm_stats = {}
plugin_stats = {}
for platform in _global_object.platforms:
platform_stats[platform.platform_name] = {
"cnt_receive": platform.platform_instance.cnt_receive,
"cnt_reply": platform.platform_instance.cnt_reply
}
for llm in _global_object.llms:
stat = llm.llm_instance.model_stat
for k in stat:
llm_stats[llm.llm_name + "#" + k] = stat[k]
llm.llm_instance.reset_model_stat()
for plugin in _global_object.cached_plugins:
plugin_stats[plugin.metadata.plugin_name] = {
"metadata": plugin.metadata,
"trig_cnt": plugin.trig_cnt
}
plugin.reset_trig_cnt()
try:
res = {
"version": _global_object.version,
"count": _global_object.cnt_total,
"ip": addr_ip,
"sys": sys.platform,
"admin": "null",
"stat_version": "moon",
"version": _global_object.version, # 版本号
"platform_stats": platform_stats, # 过去 30 分钟各消息平台交互消息数
"llm_stats": llm_stats,
"plugin_stats": plugin_stats,
"sys": sys.platform, # 系统版本
}
resp = requests.post(
'https://api.soulter.top/upload', data=json.dumps(res), timeout=5)
@@ -484,7 +435,7 @@ def upload(_global_object: GlobalObject):
_global_object.cnt_total = 0
except BaseException as e:
pass
time.sleep(10*60)
time.sleep(30*60)
def retry(n: int = 3):
'''
@@ -501,7 +452,6 @@ def retry(n: int = 3):
return wrapper
return decorator
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 type.message import AstrMessageEvent, AstrBotMessage
from type.message import *
from type.command import CommandResult
from model.platform._message_result import MessageResult
+29 -12
View File
@@ -16,6 +16,7 @@ from type.plugin import *
from type.register import *
from SparkleLogging.utils.core import LogManager
from logging import Logger
from type.types import GlobalObject
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"))
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()
if plugins is None:
return False, "未找到任何插件模块"
@@ -113,7 +126,12 @@ def plugin_reload(cached_plugins: RegisteredPlugins):
root_dir_name + "." + p, fromlist=[p])
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
try:
@@ -125,8 +143,7 @@ def plugin_reload(cached_plugins: RegisteredPlugins):
else:
metadata = PluginMetadata(
plugin_name=info['name'],
plugin_type=PluginType.COMMON if 'plugin_type' not in info else PluginType(
info['plugin_type']),
plugin_type=PluginType.COMMON if 'plugin_type' not in info else PluginType(info['plugin_type']),
author=info['author'],
desc=info['desc'],
version=info['version'],
@@ -163,7 +180,7 @@ def update_plugin_dept(path):
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()
# 删除末尾的 /
if repo_url.endswith("/"):
@@ -178,7 +195,7 @@ def install_plugin(repo_url: str, cached_plugins: RegisteredPlugins):
if os.path.exists(plugin_path):
remove_dir(plugin_path)
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:
raise Exception(err)
@@ -192,19 +209,19 @@ def get_registered_plugin(plugin_name: str, cached_plugins: RegisteredPlugins) -
return ret
def uninstall_plugin(plugin_name: str, cached_plugins: RegisteredPlugins):
plugin = get_registered_plugin(plugin_name, cached_plugins)
def uninstall_plugin(plugin_name: str, ctx: GlobalObject):
plugin = get_registered_plugin(plugin_name, ctx.cached_plugins)
if not plugin:
raise Exception("插件不存在。")
root_dir_name = plugin.root_dir_name
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)):
raise Exception("移除插件成功,但是删除插件文件夹失败。您可以手动删除该文件夹,位于 addons/plugins/ 下。")
def update_plugin(plugin_name: str, cached_plugins: RegisteredPlugins):
plugin = get_registered_plugin(plugin_name, cached_plugins)
def update_plugin(plugin_name: str, ctx: GlobalObject):
plugin = get_registered_plugin(plugin_name, ctx.cached_plugins)
if not plugin:
raise Exception("插件不存在。")
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)
repo = Repo(path=plugin_path)
repo.remotes.origin.pull()
ok, err = plugin_reload(cached_plugins)
ok, err = plugin_reload(ctx)
if not ok:
raise Exception(err)
+36 -8
View File
@@ -6,9 +6,35 @@ except BaseException as e:
has_git = False
import sys, os
import requests
import psutil
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():
py = sys.executable
terminate_child_processes()
os.execl(py, py, *sys.argv)
def find_repo() -> Repo:
@@ -78,20 +104,22 @@ def check_update() -> str:
print(f"当前版本: {curr_commit}")
print(f"最新版本: {new_commit}")
if curr_commit.startswith(new_commit):
return "当前已经是最新版本"
return f"当前已经是最新版本: v{VERSION}"
else:
update_info = f"""有新版本可用。
=== 当前版本 ===
{curr_commit}
update_info = f"""> 有新版本可用,请及时更新
# 当前版本
v{VERSION}
=== 新版本 ===
# 最新版本
{update_data[0]['version']}
=== 发布时间 ===
# 发布时间
{update_data[0]['published_at']}
=== 更新内容 ===
{update_data[0]['body']}"""
# 更新内容
---
{update_data[0]['body']}
---"""
return update_info
def update_project(update_data: list,