feat: 针对 OneBot 和 NoneBot 的消息兼容层和插件的初步适配

This commit is contained in:
Soulter
2023-11-21 14:23:47 +08:00
parent 9c3c8ff2c4
commit f0caea9026
13 changed files with 269 additions and 67 deletions
+56 -57
View File
@@ -207,6 +207,61 @@ def initBot(cfg, prov):
if 'reply_prefix' in cfg:
_global_object.reply_prefix = cfg['reply_prefix']
gu.log("--------加载机器人平台--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
thread_inst = None
admin_qq = cc.get('admin_qq', None)
admin_qqchan = cc.get('admin_qqchan', None)
if admin_qq == None:
gu.log("未设置管理者QQ号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING)
admin_qq = input("请输入管理者QQ号(必须设置): ")
gu.log("管理者QQ号设置为: " + admin_qq, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
cc.put('admin_qq', admin_qq)
if admin_qqchan == None:
gu.log("未设置管理者QQ频道用户号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING)
admin_qqchan = input("请输入管理者频道用户号(不是QQ号, 可以先回车跳过然后在频道发送指令!myid获取): ")
if admin_qqchan == "":
gu.log("跳过设置管理者频道用户号", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
else:
gu.log("管理者频道用户号设置为: " + admin_qqchan, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
cc.put('admin_qqchan', admin_qqchan)
gu.log("管理者QQ: " + admin_qq, gu.LEVEL_INFO)
gu.log("管理者频道用户号: " + admin_qqchan, gu.LEVEL_INFO)
_global_object.admin_qq = admin_qq
_global_object.admin_qqchan = admin_qqchan
# GOCQ
global gocq_bot
if 'gocqbot' in cfg and cfg['gocqbot']['enable']:
gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO)
global gocq_app, gocq_loop
gocq_loop = asyncio.new_event_loop()
gocq_bot = QQ(True, cc, gocq_loop)
thread_inst = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=False)
thread_inst.start()
else:
gocq_bot = QQ(False)
_global_object.platform_qq = gocq_bot
gu.log("机器人部署教程: https://github.com/Soulter/QQChannelChatGPT/wiki/", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
gu.log("如果有任何问题, 请在 https://github.com/Soulter/QQChannelChatGPT 上提交issue或加群322154837", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
gu.log("请给 https://github.com/Soulter/QQChannelChatGPT 点个star!", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
# QQ频道
if 'qqbot' in cfg and cfg['qqbot']['enable']:
gu.log("- 启用QQ频道机器人 -", gu.LEVEL_INFO)
global qqchannel_bot, qqchan_loop
qqchannel_bot = QQChan()
qqchan_loop = asyncio.new_event_loop()
_global_object.platform_qqchan = qqchannel_bot
thread_inst = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=False)
thread_inst.start()
# thread.join()
# 语言模型提供商
gu.log("--------加载语言模型--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
@@ -312,8 +367,6 @@ def initBot(cfg, prov):
nick_qq = tuple(nick_qq)
_global_object.nick = nick_qq
thread_inst = None
gu.log("--------加载插件--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
# 加载插件
_command = Command(None, _global_object)
@@ -327,59 +380,6 @@ def initBot(cfg, prov):
llm_command_instance[NONE_LLM] = _command
chosen_provider = NONE_LLM
gu.log("--------加载机器人平台--------", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
admin_qq = cc.get('admin_qq', None)
admin_qqchan = cc.get('admin_qqchan', None)
if admin_qq == None:
gu.log("未设置管理者QQ号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING)
admin_qq = input("请输入管理者QQ号(必须设置): ")
gu.log("管理者QQ号设置为: " + admin_qq, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
cc.put('admin_qq', admin_qq)
if admin_qqchan == None:
gu.log("未设置管理者QQ频道用户号(管理者才能使用update/plugin等指令)", gu.LEVEL_WARNING)
admin_qqchan = input("请输入管理者频道用户号(不是QQ号, 可以先回车跳过然后在频道发送指令!myid获取): ")
if admin_qqchan == "":
gu.log("跳过设置管理者频道用户号", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
else:
gu.log("管理者频道用户号设置为: " + admin_qqchan, gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
cc.put('admin_qqchan', admin_qqchan)
gu.log("管理者QQ: " + admin_qq, gu.LEVEL_INFO)
gu.log("管理者频道用户号: " + admin_qqchan, gu.LEVEL_INFO)
_global_object.admin_qq = admin_qq
_global_object.admin_qqchan = admin_qqchan
# GOCQ
global gocq_bot
if 'gocqbot' in cfg and cfg['gocqbot']['enable']:
gu.log("- 启用QQ机器人 -", gu.LEVEL_INFO)
global gocq_app, gocq_loop
gocq_loop = asyncio.new_event_loop()
gocq_bot = QQ(True, cc, gocq_loop)
thread_inst = threading.Thread(target=run_gocq_bot, args=(gocq_loop, gocq_bot, gocq_app), daemon=False)
thread_inst.start()
else:
gocq_bot = QQ(False)
_global_object.platform_qq = gocq_bot
gu.log("机器人部署教程: https://github.com/Soulter/QQChannelChatGPT/wiki/", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
gu.log("如果有任何问题, 请在 https://github.com/Soulter/QQChannelChatGPT 上提交issue或加群322154837", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
gu.log("请给 https://github.com/Soulter/QQChannelChatGPT 点个star!", gu.LEVEL_INFO, fg=gu.FG_COLORS['yellow'])
# QQ频道
if 'qqbot' in cfg and cfg['qqbot']['enable']:
gu.log("- 启用QQ频道机器人 -", gu.LEVEL_INFO)
global qqchannel_bot, qqchan_loop
qqchannel_bot = QQChan()
qqchan_loop = asyncio.new_event_loop()
_global_object.platform_qqchan = qqchannel_bot
thread_inst = threading.Thread(target=run_qqchan_bot, args=(cfg, qqchan_loop, qqchannel_bot), daemon=False)
thread_inst.start()
# thread.join()
if thread_inst == None:
input("[System-Error] 没有启用/成功启用任何机器人,程序退出")
@@ -606,7 +606,7 @@ async def oper_msg(message: Union[GroupMessage, FriendMessage, GuildMessage, Nak
if session_id in gocq_bot.waiting and gocq_bot.waiting[session_id] == '':
gocq_bot.waiting[session_id] = qq_msg
return
hit, command_result = llm_command_instance[chosen_provider].check_command(
hit, command_result = await llm_command_instance[chosen_provider].check_command(
qq_msg,
session_id,
role,
@@ -752,7 +752,6 @@ class botClient(botpy.Client):
# 收到频道消息
async def on_at_message_create(self, message: Message):
gu.log(str(message), gu.LEVEL_DEBUG, max_len=9999)
# 转换层
nakuru_guild_message = qqchannel_bot.gocq_compatible_receive(message)
gu.log(f"转换后: {str(nakuru_guild_message)}", gu.LEVEL_DEBUG, max_len=9999)
@@ -0,0 +1,2 @@
class CommandArg:
pass
+32
View File
@@ -0,0 +1,32 @@
import sys
from types import ModuleType
import asyncio
from pyppeteer import launch
async def template_to_pic(template_path, template_name, templates, pages, wait, type, quality, device_scale_factor):
browser = await launch()
page = await browser.newPage()
await page.setViewport(pages["viewport"])
await page.goto(pages["base_url"])
await asyncio.sleep(wait)
await page.evaluate('''(templates) => {
// 在页面中执行 JavaScript 代码,将数据注入到模板中
// 这里的示例代码仅供参考,具体需要根据实际情况修改
document.getElementById('css').innerText = templates.css;
document.getElementById('data').innerText = JSON.stringify(templates.data);
document.getElementById('detail').innerText = templates.detail;
}''', templates)
screenshot = await page.screenshot({
'type': type,
'quality': quality,
'deviceScaleFactor': device_scale_factor
})
await browser.close()
return screenshot
def require(module_str: str):
module = ModuleType(module_str)
sys.modules[module_str] = module
if module_str == 'nonebot_plugin_htmlrender':
module.template_to_pic = template_to_pic
+11
View File
@@ -0,0 +1,11 @@
class Driver:
def __init__(self) -> None:
self.config = {}
def on_startup(self, func):
pass
def on_bot_connect(self, func):
pass
def get_driver():
return Driver()
+2
View File
@@ -0,0 +1,2 @@
class Bot:
pass
+2
View File
@@ -0,0 +1,2 @@
class Message:
pass
@@ -0,0 +1,2 @@
class MessageEvent:
pass
@@ -0,0 +1,2 @@
class MessageSegment:
pass
+137
View File
@@ -0,0 +1,137 @@
import sys
from types import ModuleType
import asyncio
from pyppeteer import launch
from model.platform.qqchan import QQChan
from .nonebot.driver import Driver, get_driver
from .onebot.message import Message
from .onebot.message_event import MessageEvent
from .onebot.message_segment import MessageSegment
from .nonebot.command_arg import CommandArg
from .onebot.bot import Bot
from nakuru import (
GuildMessage,
GroupMessage,
FriendMessage
)
from typing import Union
NONEBOT = "nonebot"
class UnifiedBotCompatibleLayer():
def __init__(self, platform_qq_sdk: QQChan) -> None:
# 初始化兼容层
self.plugins: dict[str, CommandOper] = {}
self.platform_qq_sdk = platform_qq_sdk
self._nonebot()
self.load_plugins()
async def check_commands(self, message: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage]):
for k in self.plugins:
if message.startswith(k):
if self.plugins[k].framework_name == NONEBOT:
await self._nonebot_plugins_oper(message, message_obj, k)
async def _nonebot_plugins_oper(self, message: str, message_obj: Union[GroupMessage, FriendMessage, GuildMessage], plugin_name: str = None):
# bad implementation
# 高并发场景下,下面的代码是不安全的
while self.plugins[plugin_name].message_obj is not None:
await asyncio.sleep(1)
self.plugins[plugin_name].message_obj = message_obj
bot, event, arg = self._nonebot_adapter(message_obj)
await self.plugins[plugin_name].exec(bot, event, arg) # wrapper
def load_plugins(self):
import nonebot_plugin_gspanel.nonebot_plugin_gspanel
def _nonebot(self):
# 模拟 nonebot 模块
nonebot_module = ModuleType('nonebot')
sys.modules['nonebot'] = nonebot_module
nonebot_log_module = ModuleType('nonebot.log')
sys.modules['nonebot.log'] = nonebot_log_module
nonebot_adapter_module = ModuleType('nonebot.adapters')
sys.modules['nonebot.adapters'] = nonebot_adapter_module
nonebot_params_module = ModuleType('nonebot.params')
sys.modules['nonebot.params'] = nonebot_params_module
nonebot_drivers_module = ModuleType('nonebot.drivers')
sys.modules['nonebot.drivers'] = nonebot_drivers_module
nonebot_plugin_module = ModuleType('nonebot.plugin')
sys.modules['nonebot.plugin'] = nonebot_plugin_module
nonebot_adapter_onebot_v11_module = ModuleType('nonebot.adapters.onebot.v11')
sys.modules['nonebot.adapters.onebot.v11'] = nonebot_adapter_onebot_v11_module
nonebot_adapter_onebot_v11_event_module = ModuleType('nonebot.adapters.onebot.v11.event')
sys.modules['nonebot.adapters.onebot.v11.event'] = nonebot_adapter_onebot_v11_event_module
nonebot_adapter_onebot_v11_message_module = ModuleType('nonebot.adapters.onebot.v11.message')
sys.modules['nonebot.adapters.onebot.v11.message'] = nonebot_adapter_onebot_v11_message_module
nonebot_log_module.logger = lambda: None
nonebot_adapter_module.Message = Message
nonebot_params_module.CommandArg = CommandArg
on_command = wrap_on_command(self)
nonebot_plugin_module.on_command = on_command
nonebot_adapter_onebot_v11_module.Bot = Bot
nonebot_adapter_onebot_v11_event_module.MessageEvent = MessageEvent
nonebot_adapter_onebot_v11_message_module.MessageSegment = MessageSegment
nonebot_module.get_driver = get_driver
nonebot_module.require = require
nonebot_drivers_module.Driver = Driver
def _nonebot_adapter(self, message_obj):
bot = Bot()
event = MessageEvent()
arg = CommandArg()
# tododssss
return bot, event, arg
class BaseBot():
def __init__(self, framework_name) -> None:
self.framework_name = framework_name
class CommandOper(BaseBot):
'''
CommandOper for NoneBot
'''
def __init__(self, name, aliases=None, priority=1, block=False, _ubcl: UnifiedBotCompatibleLayer = None) -> None:
super().__init__("nonebot")
self.name = name
self.aliases = aliases
self.priority = priority
self.block = block
self.exec = None
self._ubcl = _ubcl
self.message_obj: Union[GroupMessage, FriendMessage, GuildMessage] = None
_ubcl.plugins[name] = self
def handle(self):
def decorator(func):
async def wrapper(bot: Bot, event: MessageEvent, arg: Message = CommandArg(), *args, **kwargs):
# 你可以在这里添加自定义的处理逻辑
print(f"Command {self.name} is executed.")
await func(bot, event, arg, *args, **kwargs)
self.exec = wrapper
return wrapper
return decorator
async def finish(self, msg, at_sender = True):
if self.message_obj is not None:
self._ubcl.platform_qq_sdk.send(self.message_obj, msg)
self.message_obj = None
def wrap_on_command(_ubcl: UnifiedBotCompatibleLayer):
def on_command(name, aliases=None, priority=1, block=False):
return CommandOper(name, aliases, priority, block, _ubcl = _ubcl)
return on_command
+10 -4
View File
@@ -26,21 +26,29 @@ from PIL import Image as PILImage
from cores.qqbot.global_object import GlobalObject, AstrMessageEvent
from pip._internal import main as pipmain
from .adapter.protocol_adapter import UnifiedBotCompatibleLayer
import asyncio
PLATFORM_QQCHAN = 'qqchan'
PLATFORM_GOCQ = 'gocq'
# 指令功能的基类,通用的(不区分语言模型)的指令就在这实现
class Command:
def __init__(self, provider: Provider, global_object: GlobalObject = None):
def __init__(self, provider: Provider, global_object: GlobalObject = None, unified_bot_compatible_layer: UnifiedBotCompatibleLayer = None):
self.provider = provider
self.global_object = global_object
self.unified_bot_compatible_layer = unified_bot_compatible_layer
def check_command(self,
async def check_command(self,
message,
session_id: str,
role,
platform,
message_obj):
# UBCL
await self.unified_bot_compatible_layer.check_commands(message, message_obj)
# 插件
cached_plugins = self.global_object.cached_plugins
ame = AstrMessageEvent(
@@ -70,10 +78,8 @@ 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)
if self.command_start_with(message, "myid") or self.command_start_with(message, "!myid"):
return True, self.get_my_id(message_obj)
if self.command_start_with(message, "nconf") or self.command_start_with(message, "newconf"):
+5 -3
View File
@@ -5,6 +5,7 @@ from cores.qqbot.personality import personalities
from model.platform.qq import QQ
from util import general_utils as gu
from cores.qqbot.global_object import GlobalObject
from .adapter.protocol_adapter import UnifiedBotCompatibleLayer
class CommandOpenAIOfficial(Command):
def __init__(self, provider: ProviderOpenAIOfficial, global_object: GlobalObject):
@@ -12,16 +13,17 @@ class CommandOpenAIOfficial(Command):
self.cached_plugins = {}
self.global_object = global_object
self.personality_str = ""
super().__init__(provider, global_object)
self.unified_bot_compatible_layer = UnifiedBotCompatibleLayer(self.global_object.platform_qqchan)
super().__init__(provider, global_object, self.unified_bot_compatible_layer)
def check_command(self,
async def check_command(self,
message: str,
session_id: str,
role: str,
platform: str,
message_obj):
self.platform = platform
hit, res = super().check_command(
hit, res = await super().check_command(
message,
session_id,
role,
+2 -2
View File
@@ -12,14 +12,14 @@ class CommandRevChatGPT(Command):
self.personality_str = ""
super().__init__(provider, global_object)
def check_command(self,
async def check_command(self,
message: str,
session_id: str,
role: str,
platform: str,
message_obj):
self.platform = platform
hit, res = super().check_command(
hit, res = await super().check_command(
message,
session_id,
role,
+6 -1
View File
@@ -188,4 +188,9 @@ class QQChan():
_n = NakuruGuildMessage()
_n.channel_id = channel_id
self.send_qq_msg(_n, message_chain)
def send(self, message: NakuruGuildMessage, res: list):
'''
同 send_qq_msg。回复频道消息
'''
self.send_qq_msg(message, res)