Compare commits

...

23 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
24 changed files with 647 additions and 307 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 == "":
+9 -26
View File
@@ -22,6 +22,7 @@ from util.cmd_config import init_astrbot_config_items
from type.types import GlobalObject from type.types import GlobalObject
from type.register import * from type.register import *
from type.message import AstrBotMessage from type.message import AstrBotMessage
from type.config import *
from addons.dashboard.helper import DashBoardHelper from addons.dashboard.helper import DashBoardHelper
from addons.dashboard.server import DashBoardData from addons.dashboard.server import DashBoardData
from persist.session import dbConn from persist.session import dbConn
@@ -38,9 +39,6 @@ frequency_time = 60
# 计数默认值 # 计数默认值
frequency_count = 10 frequency_count = 10
# 版本
version = '3.1.13'
# 语言模型 # 语言模型
OPENAI_OFFICIAL = 'openai_official' OPENAI_OFFICIAL = 'openai_official'
NONE_LLM = 'none_llm' NONE_LLM = 'none_llm'
@@ -56,13 +54,9 @@ baidu_judge = None
# CLI # CLI
PLATFORM_CLI = 'cli' PLATFORM_CLI = 'cli'
init_astrbot_config_items()
# 全局对象 # 全局对象
_global_object: GlobalObject = None _global_object: GlobalObject = None
# 语言模型选择
def privider_chooser(cfg): def privider_chooser(cfg):
l = [] l = []
@@ -70,21 +64,16 @@ def privider_chooser(cfg):
l.append('openai_official') l.append('openai_official')
return l return l
'''
初始化机器人
'''
def init(): def init():
'''
初始化机器人
'''
global llm_instance, llm_command_instance global llm_instance, llm_command_instance
global baidu_judge, chosen_provider global baidu_judge, chosen_provider
global frequency_count, frequency_time global frequency_count, frequency_time
global _global_object global _global_object
# 迁移旧配置 init_astrbot_config_items()
gu.try_migrate_config()
# 使用新配置
cfg = cc.get_all() cfg = cc.get_all()
_event_loop = asyncio.new_event_loop() _event_loop = asyncio.new_event_loop()
@@ -92,9 +81,10 @@ def init():
# 初始化 global_object # 初始化 global_object
_global_object = GlobalObject() _global_object = GlobalObject()
_global_object.version = version _global_object.version = VERSION
_global_object.base_config = cfg _global_object.base_config = cfg
logger.info("AstrBot v"+version) _global_object.logger = logger
logger.info("AstrBot v" + VERSION)
if 'reply_prefix' in cfg: if 'reply_prefix' in cfg:
# 适配旧版配置 # 适配旧版配置
@@ -182,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)} 个插件")
@@ -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_session(platform, session_id, 1)
db_inst.increment_stat_message(curr_ts, 1) db_inst.increment_stat_message(curr_ts, 1)
db_inst.increment_stat_platform(curr_ts, platform, 1) db_inst.increment_stat_platform(curr_ts, platform, 1)
_global_object.cnt_total += 1
async def oper_msg(message: AstrBotMessage, async def oper_msg(message: AstrBotMessage,
@@ -453,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])}")
+7 -10
View File
@@ -5,12 +5,11 @@ import warnings
import traceback import traceback
import threading import threading
from logging import Formatter, Logger from logging import Formatter, Logger
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 = """
___ _______.___________..______ .______ ______ .___________. ___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | | / \ / | || _ \ | _ \ / __ \ | |
@@ -34,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:
@@ -64,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()
@@ -77,9 +76,9 @@ def check_env():
exit() exit()
if __name__ == "__main__": if __name__ == "__main__":
update_dept()
# 设置代理 make_necessary_dirs()
from util.cmd_config import CmdConfig try_migrate_config()
cc = CmdConfig() cc = CmdConfig()
http_proxy = cc.get("http_proxy") http_proxy = cc.get("http_proxy")
https_proxy = cc.get("https_proxy") https_proxy = cc.get("https_proxy")
@@ -88,8 +87,6 @@ if __name__ == "__main__":
if https_proxy: if https_proxy:
os.environ['HTTPS_PROXY'] = https_proxy os.environ['HTTPS_PROXY'] = https_proxy
os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com' os.environ['NO_PROXY'] = 'https://api.sgroup.qq.com'
update_dept()
from SparkleLogging.utils.core import LogManager from SparkleLogging.utils.core import LogManager
logger = LogManager.GetLogger( logger = LogManager.GetLogger(
+43 -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 *
@@ -64,6 +65,8 @@ class Command:
result = await plugin.plugin_instance.run(ame) result = await plugin.plugin_instance.run(ame)
else: else:
result = await asyncio.to_thread(plugin.plugin_instance.run, ame) result = await asyncio.to_thread(plugin.plugin_instance.run, ame)
if not result:
continue
if isinstance(result, CommandResult): if isinstance(result, CommandResult):
hit = result.hit hit = result.hit
res = result._result_tuple() res = result._result_tuple()
@@ -73,6 +76,7 @@ class Command:
else: else:
raise TypeError("插件返回值格式错误。") raise TypeError("插件返回值格式错误。")
if hit: if hit:
plugin.trig()
logger.debug("hit plugin: " + plugin.metadata.plugin_name) logger.debug("hit plugin: " + plugin.metadata.plugin_name)
return True, res return True, res
except TypeError as e: except TypeError as e:
@@ -94,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
@@ -116,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"
@@ -157,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:
@@ -229,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
@@ -265,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"
+17 -19
View File
@@ -84,6 +84,7 @@ class CommandOpenAIOfficial(Command):
for model in models: for model in models:
ret += f"\n{i}. {model.id}" ret += f"\n{i}. {model.id}"
i += 1 i += 1
ret += "\nTips: 使用 /model 模型名/编号,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
logger.debug(ret) logger.debug(ret)
return True, ret, "models" return True, ret, "models"
@@ -93,31 +94,28 @@ 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: else:
f = False self.provider.set_model(model)
for m in models: return True, f"模型已设置为 {model} (自定义)", "model"
if model == m.id:
f = True
break
if not f:
return True, "模型不存在或输入非法", "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()
commands[''] = '调用 OpenAI DallE 模型生成图片' commands[''] = '调用 OpenAI DallE 模型生成图片'
commands['set'] = '人格设置面板' commands['/set'] = '人格设置面板'
commands['status'] = '查看 Api Key 状态和配置信息' commands['/status'] = '查看 Api Key 状态和配置信息'
commands['token'] = '查看本轮会话 token' commands['/token'] = '查看本轮会话 token'
commands['reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt' commands['/reset'] = '重置当前与 LLM 的会话,但保留人格(system prompt'
commands['reset p'] = '重置当前与 LLM 的会话,并清除人格。' commands['/reset p'] = '重置当前与 LLM 的会话,并清除人格。'
commands['/models'] = '获取当前可用的模型'
commands['/model'] = '更换模型'
return True, await super().help_messager(commands, self.platform, self.global_object.cached_plugins), "help" return True, await super().help_messager(commands, self.platform, self.global_object.cached_plugins), "help"
+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
+10 -4
View File
@@ -14,34 +14,40 @@ class Platform():
初始化平台的各种接口 初始化平台的各种接口
''' '''
self.message_handler = message_handler self.message_handler = message_handler
self.cnt_receive = 0
self.cnt_reply = 0
pass pass
@abc.abstractmethod @abc.abstractmethod
async def handle_msg(): async def handle_msg(self):
''' '''
处理到来的消息 处理到来的消息
''' '''
self.cnt_receive += 1
pass pass
@abc.abstractmethod @abc.abstractmethod
async def reply_msg(): async def reply_msg(self):
''' '''
回复消息(被动发送) 回复消息(被动发送)
''' '''
self.cnt_reply += 1
pass pass
@abc.abstractmethod @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 pass
@abc.abstractmethod @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() 发送消息(主动发送)同 send_msg()
''' '''
self.cnt_reply += 1
pass pass
def parse_message_outline(self, message: Union[GuildMessage, GroupMessage, FriendMessage, str, list]) -> str: def parse_message_outline(self, message: Union[GuildMessage, GroupMessage, FriendMessage, str, list]) -> str:
+9 -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 (
@@ -104,6 +105,7 @@ class QQGOCQ(Platform):
self.client.run() self.client.run()
async def handle_msg(self, message: AstrBotMessage): async def handle_msg(self, message: AstrBotMessage):
await super().handle_msg()
logger.info( logger.info(
f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}") f"{message.sender.nickname}/{message.sender.user_id} -> {self.parse_message_outline(message)}")
@@ -176,6 +178,7 @@ class QQGOCQ(Platform):
async def reply_msg(self, async def reply_msg(self,
message: Union[AstrBotMessage, GuildMessage, GroupMessage, FriendMessage], message: Union[AstrBotMessage, GuildMessage, GroupMessage, FriendMessage],
result_message: list): result_message: list):
await super().reply_msg()
""" """
插件开发者请使用send方法, 可以不用直接调用这个方法。 插件开发者请使用send方法, 可以不用直接调用这个方法。
""" """
@@ -211,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
@@ -254,6 +258,7 @@ class QQGOCQ(Platform):
提供给插件的发送QQ消息接口。 提供给插件的发送QQ消息接口。
参数说明:第一个参数可以是消息对象,也可以是QQ群号。第二个参数是消息内容(消息内容可以是消息链列表,也可以是纯文字信息)。 参数说明:第一个参数可以是消息对象,也可以是QQ群号。第二个参数是消息内容(消息内容可以是消息链列表,也可以是纯文字信息)。
''' '''
await super().reply_msg()
try: try:
await self.reply_msg(message, result_message) await self.reply_msg(message, result_message)
except BaseException as e: except BaseException as e:
@@ -265,22 +270,17 @@ class QQGOCQ(Platform):
''' '''
同 send_msg() 同 send_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
+51 -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(
@@ -102,6 +104,7 @@ class QQOfficial(Platform):
) )
async def handle_msg(self, message: AstrBotMessage): async def handle_msg(self, message: AstrBotMessage):
await super().handle_msg()
assert isinstance(message.raw_message, (botpy.message.Message, assert isinstance(message.raw_message, (botpy.message.Message,
botpy.message.GroupMessage, botpy.message.DirectMessage)) botpy.message.GroupMessage, botpy.message.DirectMessage))
is_group = message.type != MessageType.FRIEND_MESSAGE is_group = message.type != MessageType.FRIEND_MESSAGE
@@ -154,6 +157,7 @@ class QQOfficial(Platform):
''' '''
回复频道消息 回复频道消息
''' '''
await super().reply_msg()
if isinstance(message, AstrBotMessage): if isinstance(message, AstrBotMessage):
source = message.raw_message source = message.raw_message
else: else:
@@ -167,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,
@@ -213,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:
+5
View File
@@ -73,6 +73,7 @@ class ProviderOpenAIOfficial(Provider):
base_url=self.base_url base_url=self.base_url
) )
self.model_configs: Dict = cfg['chatGPTConfigs'] 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.image_generator_model_configs: Dict = self.cc.get('openai_image_generate', None)
self.session_memory: Dict[str, List] = {} # 会话记忆 self.session_memory: Dict[str, List] = {} # 会话记忆
self.session_memory_lock = threading.Lock() self.session_memory_lock = threading.Lock()
@@ -289,6 +290,7 @@ class ProviderOpenAIOfficial(Provider):
extra_conf: Dict = None, extra_conf: Dict = None,
**kwargs **kwargs
) -> str: ) -> str:
super().accu_model_stat()
if not session_id: if not session_id:
session_id = "unknown" session_id = "unknown"
if "unknown" in self.session_memory: if "unknown" in self.session_memory:
@@ -421,6 +423,7 @@ class ProviderOpenAIOfficial(Provider):
''' '''
retry = 0 retry = 0
conf = self.image_generator_model_configs conf = self.image_generator_model_configs
super().accu_model_stat(model=conf['model'])
if not conf: if not conf:
logger.error("OpenAI 图片生成模型配置不存在。") logger.error("OpenAI 图片生成模型配置不存在。")
raise Exception("OpenAI 图片生成模型配置不存在。") raise Exception("OpenAI 图片生成模型配置不存在。")
@@ -481,6 +484,8 @@ class ProviderOpenAIOfficial(Provider):
def set_model(self, model: str): def set_model(self, model: str):
self.model_configs['model'] = model self.model_configs['model'] = model
self.cc.put_by_dot_str("openai.chatGPTConfigs.model", model)
super().set_curr_model(model)
def get_configs(self): def get_configs(self):
return self.model_configs return self.model_configs
+26 -3
View File
@@ -1,4 +1,27 @@
from collections import defaultdict
class Provider: 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, async def text_chat(self,
prompt: str, prompt: str,
session_id: str, session_id: str,
@@ -18,7 +41,7 @@ class Provider:
extra_conf: 额外配置 extra_conf: 额外配置
default_personality: 默认人格 default_personality: 默认人格
''' '''
raise NotImplementedError raise NotImplementedError()
async def image_generate(self, prompt, session_id, **kwargs) -> str: async def image_generate(self, prompt, session_id, **kwargs) -> str:
''' '''
@@ -26,10 +49,10 @@ class Provider:
prompt: 提示词 prompt: 提示词
session_id: 会话id session_id: 会话id
''' '''
raise NotImplementedError raise NotImplementedError()
async def forget(self, session_id=None) -> bool: async def forget(self, session_id=None) -> bool:
''' '''
重置会话 重置会话
''' '''
raise NotImplementedError raise NotImplementedError()
+1 -1
View File
@@ -18,7 +18,7 @@ class CommandResult():
用于在Command中返回多个值 用于在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.hit = hit
self.success = success self.success = success
self.message_chain = message_chain 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 from dataclasses import dataclass
class PluginType(Enum): class PluginType(Enum):
PLATFORM = 'platfrom' # 平台类插件。 PLATFORM = 'platform' # 平台类插件。
LLM = 'llm' # 大语言模型类插件 LLM = 'llm' # 大语言模型类插件
COMMON = 'common' # 其他插件 COMMON = 'common' # 其他插件
+7
View File
@@ -15,6 +15,13 @@ class RegisteredPlugin:
module_path: str module_path: str
module: ModuleType module: ModuleType
root_dir_name: str 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: def __str__(self) -> str:
return f"RegisteredPlugin({self.metadata}, {self.module_path}, {self.root_dir_name})" 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 type.register import *
from typing import List from typing import List
from logging import Logger
class GlobalObject: class GlobalObject:
''' '''
@@ -15,9 +17,10 @@ class GlobalObject:
web_search: bool # 是否开启了网页搜索 web_search: bool # 是否开启了网页搜索
reply_prefix: str # 回复前缀 reply_prefix: str # 回复前缀
unique_session: bool # 是否开启了独立会话 unique_session: bool # 是否开启了独立会话
cnt_total: int # 总消息数
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 的昵称
@@ -26,7 +29,6 @@ class GlobalObject:
self.web_search = False # 是否开启了网页搜索 self.web_search = False # 是否开启了网页搜索
self.reply_prefix = None self.reply_prefix = None
self.unique_session = False self.unique_session = False
self.cnt_total = 0
self.platforms = [] self.platforms = []
self.llms = [] self.llms = []
self.default_personality = None self.default_personality = None
+26
View File
@@ -1,5 +1,6 @@
import os import os
import json import json
import yaml
from typing import Union from typing import Union
cpath = "data/cmd_config.json" cpath = "data/cmd_config.json"
@@ -117,3 +118,28 @@ def init_astrbot_config_items():
cc.init_attributes("https_proxy", "") cc.init_attributes("https_proxy", "")
cc.init_attributes("dashboard_username", "") cc.init_attributes("dashboard_username", "")
cc.init_attributes("dashboard_password", "") 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 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
@@ -422,21 +377,6 @@ def create_markdown_image(text: str):
raise e 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(): def get_local_ip_addresses():
ip = '' ip = ''
try: try:
@@ -450,31 +390,42 @@ 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):
'''
上传相关非敏感统计数据
'''
time.sleep(10)
while True: 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: try:
res = { res = {
"version": _global_object.version, "stat_version": "moon",
"count": _global_object.cnt_total, "version": _global_object.version, # 版本号
"ip": addr_ip, "platform_stats": platform_stats, # 过去 30 分钟各消息平台交互消息数
"sys": sys.platform, "llm_stats": llm_stats,
"admin": "null", "plugin_stats": plugin_stats,
"sys": sys.platform, # 系统版本
} }
resp = requests.post( resp = requests.post(
'https://api.soulter.top/upload', data=json.dumps(res), timeout=5) '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 _global_object.cnt_total = 0
except BaseException as e: except BaseException as e:
pass pass
time.sleep(10*60) time.sleep(30*60)
def retry(n: int = 3): def retry(n: int = 3):
''' '''
@@ -501,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)
+36 -8
View File
@@ -6,9 +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 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:
@@ -78,20 +104,22 @@ def check_update() -> str:
print(f"当前版本: {curr_commit}") print(f"当前版本: {curr_commit}")
print(f"最新版本: {new_commit}") print(f"最新版本: {new_commit}")
if curr_commit.startswith(new_commit): if curr_commit.startswith(new_commit):
return "当前已经是最新版本" return f"当前已经是最新版本: v{VERSION}"
else: else:
update_info = f"""有新版本可用。 update_info = f"""> 有新版本可用,请及时更新
=== 当前版本 === # 当前版本
{curr_commit} 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,