Merge pull request #100 from RockChinQ/plugin-support

Plugin support
This commit is contained in:
Rock Chin 2023-01-17 13:35:54 +08:00 committed by GitHub
commit d51a0a644a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1464 additions and 312 deletions

4
.gitignore vendored
View File

@ -4,4 +4,6 @@ __pycache__/
database.db
qchatgpt.log
config.py
banlist.py
banlist.py
plugins/
!plugins/__init__.py

View File

@ -99,13 +99,26 @@ python3 main.py
- 如提示安装`uvicorn`或`hypercorn`请*不要*安装这两个不是必需的目前存在未知原因bug
- 如报错`TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary`, 请参考 [此处](https://github.com/RockChinQ/QChatGPT/issues/5)
</details>
## 🚀使用
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
## 🧩插件生态
现已支持自行开发插件对功能进行扩展或自定义程序行为
详见[Wiki插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
开发教程见[Wiki插件开发页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
### 示例插件
在`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
- `cmdcn` - 主程序指令中文形式
- `hello_plugin` - 在收到消息`hello`时回复相应消息
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
## 👍赞赏
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/>

70
main.py
View File

@ -39,6 +39,38 @@ def init_db():
known_exception_caught = False
def reset_logging():
assert os.path.exists('config.py')
config = importlib.import_module('config')
import pkg.utils.context
if pkg.utils.context.context['logger_handler'] is not None:
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
for handler in logging.getLogger().handlers:
logging.getLogger().removeHandler(handler)
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
filename='qchatgpt.log', # log日志输出的文件位置和文件名
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
# 日志输出的格式
# -8表示占位符让输出左对齐输出长度都为8位
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
)
sh = logging.StreamHandler()
sh.setLevel(config.logging_level)
sh.setFormatter(colorlog.ColoredFormatter(
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
"%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors=log_colors_config
))
logging.getLogger().addHandler(sh)
return sh
def main(first_time_init=False):
global known_exception_caught
@ -52,25 +84,7 @@ def main(first_time_init=False):
import pkg.utils.context
pkg.utils.context.set_config(config)
if pkg.utils.context.context['logger_handler'] is not None:
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
filename='qchatgpt.log', # log日志输出的文件位置和文件名
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
# 日志输出的格式
# -8表示占位符让输出左对齐输出长度都为8位
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
)
sh = logging.StreamHandler()
sh.setLevel(config.logging_level)
sh.setFormatter(colorlog.ColoredFormatter(
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
"%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors=log_colors_config
))
logging.getLogger().addHandler(sh)
sh = reset_logging()
# 检查是否设置了管理员
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0):
@ -117,9 +131,16 @@ def main(first_time_init=False):
timeout=config.process_message_timeout, retry=config.retry_times,
first_time_init=first_time_init)
# 加载插件
import pkg.plugin.host
pkg.plugin.host.load_plugins()
pkg.plugin.host.initialize_plugins()
if first_time_init: # 不是热重载之后的启动,则不启动新的bot线程
import mirai.exceptions
def run_bot_wrapper():
global known_exception_caught
try:
@ -155,7 +176,7 @@ def main(first_time_init=False):
known_exception_caught = True
else:
logging.error(
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/issues 查找或提issue".format(e))
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e))
known_exception_caught = True
raise e
@ -201,6 +222,9 @@ def stop():
import pkg.qqbot.manager
import pkg.openai.session
try:
import pkg.plugin.host
pkg.plugin.host.unload_plugins()
qqbot_inst = pkg.utils.context.get_qqbot_manager()
assert isinstance(qqbot_inst, pkg.qqbot.manager.QQBotManager)
@ -230,6 +254,12 @@ if __name__ == '__main__':
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
try:
try:
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.ensure_dulwich()
except:
pass
from dulwich import porcelain
repo = porcelain.open_repo('.')

View File

@ -40,6 +40,9 @@ class DataGatherer:
except:
return
def get_usage(self, key_md5):
return self.usage[key_md5] if key_md5 in self.usage else {}
def report_text_model_usage(self, model, text):
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()

View File

@ -2,10 +2,8 @@
import hashlib
import logging
import pkg.database.manager
import pkg.qqbot.manager
import pkg.utils.context
import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
class KeysManager:
api_key = {}
@ -39,13 +37,10 @@ class KeysManager:
elif type(api_key) is list:
for i in range(len(api_key)):
self.api_key[str(i)] = api_key[i]
self.auto_switch()
# 从usage中删除未加载的api-key的记录
# 不删了也许会运行时添加曾经有记录的api-key
if 'exceeded_keys' in pkg.utils.context.context and pkg.utils.context.context['exceeded_keys'] is not None:
self.exceeded = pkg.utils.context.context['exceeded_keys']
self.auto_switch()
# 根据tested自动切换到可用的api-key
# 返回是否切换成功, 切换后的api-key的别名
@ -53,7 +48,16 @@ class KeysManager:
for key_name in self.api_key:
if self.api_key[key_name] not in self.exceeded:
self.using_key = self.api_key[key_name]
logging.info("使用api-key:" + key_name)
# 触发插件事件
args = {
"key_name": key_name,
"key_list": self.api_key.keys()
}
_ = plugin_host.emit(plugin_models.KeySwitched, **args)
return True, key_name
self.using_key = list(self.api_key.values())[0]

View File

@ -6,6 +6,9 @@ import pkg.openai.manager
import pkg.database.manager
import pkg.utils.context
import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
# 运行时保存的所有session
sessions = {}
@ -120,6 +123,17 @@ class Session:
config = pkg.utils.context.get_config()
if int(time.time()) - self.last_interact_timestamp > config.session_expire_time:
logging.info('session {} 已过期'.format(self.name))
# 触发插件事件
args = {
'session_name': self.name,
'session': self,
'session_expire_time': config.session_expire_time
}
event = pkg.plugin.host.emit(plugin_models.SessionExpired, **args)
if event.is_prevented_default():
return
self.reset(expired=True, schedule_new=False)
# 删除此session
@ -131,6 +145,18 @@ class Session:
def append(self, text: str) -> str:
self.last_interact_timestamp = int(time.time())
# 触发插件事件
if self.prompt == self.get_default_prompt():
args = {
'session_name': self.name,
'session': self,
'default_prompt': self.prompt,
}
event = pkg.plugin.host.emit(plugin_models.SessionFirstMessageReceived, **args)
if event.is_prevented_default():
return None
# max_rounds = config.prompt_submit_round_amount if hasattr(config, 'prompt_submit_round_amount') else 7
config = pkg.utils.context.get_config()
max_rounds = 1000 # 不再限制回合数
@ -220,6 +246,15 @@ class Session:
if self.prompt != self.get_default_prompt():
self.persistence()
if explicit:
# 触发插件事件
args = {
'session_name': self.name,
'session': self
}
# 此事件不支持阻止默认行为
_ = pkg.plugin.host.emit(plugin_models.SessionExplicitReset, **args)
pkg.utils.context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
if expired:

0
pkg/plugin/__init__.py Normal file
View File

273
pkg/plugin/host.py Normal file
View File

@ -0,0 +1,273 @@
# 插件管理模块
import asyncio
import logging
import importlib
import os
import pkgutil
import sys
import traceback
import pkg.utils.context as context
import pkg.plugin.switch as switch
from mirai import Mirai
__plugins__ = {}
"""
插件列表
示例:
{
"example": {
"path": "plugins/example/main.py",
"enabled: True,
"name": "example",
"description": "example",
"version": "0.0.1",
"author": "RockChinQ",
"class": <class 'plugins.example.ExamplePlugin'>,
"hooks": {
"person_message": [
<function ExamplePlugin.person_message at 0x0000020E1D1B8D38>
]
},
"instance": None
}
}"""
__current_module_path__ = ""
def walk_plugin_path(module, prefix='', path_prefix=''):
global __current_module_path__
"""遍历插件路径"""
for item in pkgutil.iter_modules(module.__path__):
if item.ispkg:
logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name))
walk_plugin_path(__import__(module.__name__ + '.' + item.name, fromlist=['']),
prefix + item.name + '.', path_prefix + item.name + '/')
else:
logging.debug("扫描插件模块: plugins/{}".format(path_prefix + item.name + '.py'))
logging.info('加载模块: plugins/{}'.format(path_prefix + item.name + '.py'))
__current_module_path__ = "plugins/"+path_prefix + item.name + '.py'
importlib.import_module(module.__name__ + '.' + item.name)
def load_plugins():
""" 加载插件 """
logging.info("加载插件")
PluginHost()
walk_plugin_path(__import__('plugins'))
logging.debug(__plugins__)
# 加载开关数据
switch.load_switch()
def initialize_plugins():
""" 初始化插件 """
logging.info("初始化插件")
for plugin in __plugins__.values():
if not plugin['enabled']:
continue
try:
plugin['instance'] = plugin["class"](plugin_host=context.get_plugin_host())
except:
logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
def unload_plugins():
""" 卸载插件 """
for plugin in __plugins__.values():
if plugin['enabled'] and plugin['instance'] is not None:
if not hasattr(plugin['instance'], '__del__'):
logging.warning("插件{}没有定义析构函数".format(plugin['name']))
else:
try:
plugin['instance'].__del__()
logging.info("卸载插件: {}".format(plugin['name']))
except:
logging.error("插件{}卸载时发生错误: {}".format(plugin['name'], sys.exc_info()))
def install_plugin(repo_url: str):
""" 安装插件从git储存库获取并解决依赖 """
try:
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.ensure_dulwich()
except:
pass
try:
import dulwich
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
from dulwich import porcelain
logging.info("克隆插件储存库: {}".format(repo_url))
repo = porcelain.clone(repo_url, "plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/", checkout=True)
# 检查此目录是否包含requirements.txt
if os.path.exists("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt"):
logging.info("检测到requirements.txt正在安装依赖")
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.install_requirements("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt")
import main
main.reset_logging()
class EventContext:
""" 事件上下文 """
eid = 0
"""事件编号"""
name = ""
__prevent_default__ = False
""" 是否阻止默认行为 """
__prevent_postorder__ = False
""" 是否阻止后续插件的执行 """
__return_value__ = {}
""" 返回值
示例:
{
"example": [
'value1',
'value2',
3,
4,
{
'key1': 'value1',
},
['value1', 'value2']
]
}
"""
def add_return(self, key: str, ret):
"""添加返回值"""
if key not in self.__return_value__:
self.__return_value__[key] = []
self.__return_value__[key].append(ret)
def get_return(self, key: str):
"""获取key的所有返回值"""
if key in self.__return_value__:
return self.__return_value__[key]
return None
def get_return_value(self, key: str):
"""获取key的首个返回值"""
if key in self.__return_value__:
return self.__return_value__[key][0]
return None
def prevent_default(self):
"""阻止默认行为"""
self.__prevent_default__ = True
def prevent_postorder(self):
"""阻止后续插件执行"""
self.__prevent_postorder__ = True
def is_prevented_default(self):
"""是否阻止默认行为"""
return self.__prevent_default__
def is_prevented_postorder(self):
"""是否阻止后序插件执行"""
return self.__prevent_postorder__
def __init__(self, name: str):
self.name = name
self.eid = EventContext.eid
self.__prevent_default__ = False
self.__prevent_postorder__ = False
self.__return_value__ = {}
EventContext.eid += 1
def emit(event_name: str, **kwargs) -> EventContext:
""" 触发事件 """
import pkg.utils.context as context
if context.get_plugin_host() is None:
return None
return context.get_plugin_host().emit(event_name, **kwargs)
class PluginHost:
"""插件宿主"""
def __init__(self):
context.set_plugin_host(self)
def get_runtime_context(self) -> context:
"""获取运行时上下文pkg.utils.context模块的对象
此上下文用于和主程序其他模块交互数据库QQ机器人OpenAI接口等
详见pkg.utils.context模块
其中的context变量保存了其他重要模块的类对象可以使用这些对象进行交互
"""
return context
def get_bot(self) -> Mirai:
"""获取机器人对象"""
return context.get_qqbot_manager().bot
def send_person_message(self, person, message):
"""发送私聊消息"""
asyncio.run(self.get_bot().send_friend_message(person, message))
def send_group_message(self, group, message):
"""发送群消息"""
asyncio.run(self.get_bot().send_group_message(group, message))
def notify_admin(self, message):
"""通知管理员"""
context.get_qqbot_manager().notify_admin(message)
def emit(self, event_name: str, **kwargs) -> EventContext:
""" 触发事件 """
event_context = EventContext(event_name)
logging.debug("触发事件: {} ({})".format(event_name, event_context.eid))
for plugin in __plugins__.values():
if not plugin['enabled']:
continue
if plugin['instance'] is None:
# 从关闭状态切到开启状态之后,重新加载插件
try:
plugin['instance'] = plugin["class"]()
except:
logging.error("插件{}初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
continue
for hook in plugin['hooks'].get(event_name, []):
try:
already_prevented_default = event_context.is_prevented_default()
kwargs['host'] = context.get_plugin_host()
kwargs['event'] = event_context
hook(plugin['instance'], **kwargs)
if event_context.is_prevented_default() and not already_prevented_default:
logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name))
if event_context.is_prevented_postorder():
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin['name']))
break
except Exception as e:
logging.error("插件{}触发事件{}时发生错误".format(plugin['name'], event_name))
logging.error(traceback.format_exc())
return event_context

219
pkg/plugin/models.py Normal file
View File

@ -0,0 +1,219 @@
import logging
import pkg.plugin.host as host
import pkg.utils.context
PersonMessageReceived = "person_message_received"
"""收到私聊消息时,在判断是否应该响应前触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
message_chain: mirai.models.message.MessageChain 消息链
"""
GroupMessageReceived = "group_message_received"
"""收到群聊消息时,在判断是否应该响应前触发(所有群消息)
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
message_chain: mirai.models.message.MessageChain 消息链
"""
PersonNormalMessageReceived = "person_normal_message_received"
"""判断为应该处理的私聊普通消息时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
text_message: str 消息文本
returns (optional):
alter: str 修改后的消息文本
reply: list 回复消息组件列表
"""
PersonCommandSent = "person_command_sent"
"""判断为应该处理的私聊指令时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
command: str 指令
params: list[str] 参数列表
text_message: str 完整指令文本
is_admin: bool 是否为管理员
returns (optional):
alter: str 修改后的完整指令文本
reply: list 回复消息组件列表
"""
GroupNormalMessageReceived = "group_normal_message_received"
"""判断为应该处理的群聊普通消息时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
text_message: str 消息文本
returns (optional):
alter: str 修改后的消息文本
reply: list 回复消息组件列表
"""
GroupCommandSent = "group_command_sent"
"""判断为应该处理的群聊指令时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
command: str 指令
params: list[str] 参数列表
text_message: str 完整指令文本
is_admin: bool 是否为管理员
returns (optional):
alter: str 修改后的完整指令文本
reply: list 回复消息组件列表
"""
NormalMessageResponded = "normal_message_responded"
"""获取到对普通消息的文字响应时触发
kwargs:
launcher_type: str 发起对象类型(group/person)
launcher_id: int 发起对象ID(群号/QQ号)
sender_id: int 发送者ID(QQ号)
session: pkg.openai.session.Session 会话对象
prefix: str 回复文字消息的前缀
response_text: str 响应文本
returns (optional):
prefix: str 修改后的回复文字消息的前缀
reply: list 替换回复消息组件列表
"""
SessionFirstMessageReceived = "session_first_message_received"
"""会话被第一次交互时触发
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
session: pkg.openai.session.Session 会话对象
default_prompt: str 预设值
"""
SessionExplicitReset = "session_reset"
"""会话被用户手动重置时触发,此事件不支持阻止默认行为
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
session: pkg.openai.session.Session 会话对象
"""
SessionExpired = "session_expired"
"""会话过期时触发
kwargs:
session_name: str 会话名称(<launcher_type>_<launcher_id>)
session: pkg.openai.session.Session 会话对象
session_expire_time: int 已设置的会话过期时间()
"""
KeyExceeded = "key_exceeded"
"""api-key超额时触发
kwargs:
key_name: str 超额的api-key名称
usage: dict 超额的api-key使用情况
exceeded_keys: list[str] 超额的api-key列表
"""
KeySwitched = "key_switched"
"""api-key超额切换成功时触发此事件不支持阻止默认行为
kwargs:
key_name: str 切换成功的api-key名称
key_list: list[str] api-key列表
"""
def on(event: str):
"""注册事件监听器
:param
event: str 事件名称
"""
return Plugin.on(event)
__current_registering_plugin__ = ""
class Plugin:
host: host.PluginHost
"""插件宿主,提供插件的一些基础功能"""
@classmethod
def on(cls, event):
"""事件处理器装饰器
:param
event: 事件类型
:return:
None
"""
def wrapper(func):
plugin_hooks = host.__plugins__[__current_registering_plugin__]["hooks"]
if event not in plugin_hooks:
plugin_hooks[event] = []
plugin_hooks[event].append(func)
host.__plugins__[__current_registering_plugin__]["hooks"] = plugin_hooks
return func
return wrapper
def register(name: str, description: str, version: str, author: str):
"""注册插件, 此函数作为装饰器使用
Args:
name (str): 插件名称
description (str): 插件描述
version (str): 插件版本
author (str): 插件作者
Returns:
None
"""
global __current_registering_plugin__
__current_registering_plugin__ = name
host.__plugins__[name] = {
"name": name,
"description": description,
"version": version,
"author": author,
"hooks": {},
"path": host.__current_module_path__,
"enabled": True,
"instance": None,
}
def wrapper(cls: Plugin):
cls.name = name
cls.description = description
cls.version = version
cls.author = author
cls.host = pkg.utils.context.get_plugin_host()
cls.enabled = True
cls.path = host.__current_module_path__
# 存到插件列表
host.__plugins__[name]["class"] = cls
logging.info("插件注册完成: n='{}', d='{}', v={}, a='{}' ({})".format(name, description, version, author, cls))
return cls
return wrapper

87
pkg/plugin/switch.py Normal file
View File

@ -0,0 +1,87 @@
# 控制插件的开关
import json
import logging
import os
import pkg.plugin.host as host
def wrapper_dict_from_plugin_list() -> dict:
""" 将插件列表转换为开关json """
switch = {}
for plugin_name in host.__plugins__:
plugin = host.__plugins__[plugin_name]
switch[plugin_name] = {
"path": plugin["path"],
"enabled": plugin["enabled"],
}
return switch
def apply_switch(switch: dict):
"""将开关数据应用到插件列表中"""
for plugin_name in switch:
host.__plugins__[plugin_name]["enabled"] = switch[plugin_name]["enabled"]
def dump_switch():
""" 保存开关数据 """
logging.debug("保存开关数据")
# 将开关数据写入plugins/switch.json
switch = wrapper_dict_from_plugin_list()
with open("plugins/switch.json", "w", encoding="utf-8") as f:
json.dump(switch, f, indent=4, ensure_ascii=False)
def load_switch():
""" 加载开关数据 """
logging.debug("加载开关数据")
# 读取plugins/switch.json
switch = {}
# 检查文件是否存在
if not os.path.exists("plugins/switch.json"):
# 不存在则创建
with open("plugins/switch.json", "w", encoding="utf-8") as f:
json.dump(switch, f, indent=4, ensure_ascii=False)
with open("plugins/switch.json", "r", encoding="utf-8") as f:
switch = json.load(f)
if switch is None:
switch = {}
switch_modified = False
switch_copy = switch.copy()
# 检查switch中多余的和path不相符的
for plugin_name in switch_copy:
if plugin_name not in host.__plugins__:
del switch[plugin_name]
switch_modified = True
elif switch[plugin_name]["path"] != host.__plugins__[plugin_name]["path"]:
# 删除此不相符的
del switch[plugin_name]
switch_modified = True
# 检查plugin中多余的
for plugin_name in host.__plugins__:
if plugin_name not in switch:
switch[plugin_name] = {
"path": host.__plugins__[plugin_name]["path"],
"enabled": host.__plugins__[plugin_name]["enabled"],
}
switch_modified = True
# 如果switch有修改保存
if switch_modified:
dump_switch()
# 应用开关数据
apply_switch(switch)

313
pkg/qqbot/command.py Normal file
View File

@ -0,0 +1,313 @@
# 指令处理模块
import logging
import json
import datetime
import os
import threading
import pkg.openai.session
import pkg.openai.manager
import pkg.utils.reloader
import pkg.utils.updater
import pkg.utils.context
import pkg.qqbot.message
from mirai import Image
def config_operation(cmd, params):
reply = []
config = pkg.utils.context.get_config()
reply_str = ""
if len(params) == 0:
reply = ["[bot]err:请输入配置项"]
else:
cfg_name = params[0]
if cfg_name == 'all':
reply_str = "[bot]所有配置项:\n\n"
for cfg in dir(config):
if not cfg.startswith('__') and not cfg == 'logging':
# 根据配置项类型进行格式化如果是字典则转换为json并格式化
if isinstance(getattr(config, cfg), str):
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
elif isinstance(getattr(config, cfg), dict):
# 不进行unicode转义并格式化
reply_str += "{}: {}\n".format(cfg,
json.dumps(getattr(config, cfg),
ensure_ascii=False, indent=4))
else:
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
reply = [reply_str]
elif cfg_name in dir(config):
if len(params) == 1:
# 按照配置项类型进行格式化
if isinstance(getattr(config, cfg_name), str):
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
elif isinstance(getattr(config, cfg_name), dict):
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
json.dumps(getattr(config, cfg_name),
ensure_ascii=False, indent=4))
else:
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
reply = [reply_str]
else:
cfg_value = " ".join(params[1:])
# 类型转换如果是json则转换为字典
if cfg_value == 'true':
cfg_value = True
elif cfg_value == 'false':
cfg_value = False
elif cfg_value.isdigit():
cfg_value = int(cfg_value)
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
cfg_value = json.loads(cfg_value)
else:
try:
cfg_value = float(cfg_value)
except ValueError:
pass
# 检查类型是否匹配
if isinstance(getattr(config, cfg_name), type(cfg_value)):
setattr(config, cfg_name, cfg_value)
pkg.utils.context.set_config(config)
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
else:
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
else:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
return reply
def plugin_operation(cmd, params, is_admin):
reply = []
import pkg.plugin.host as plugin_host
import pkg.utils.updater as updater
plugin_list = plugin_host.__plugins__
if len(params) == 0:
reply_str = "[bot]所有插件({}):\n\n".format(len(plugin_list))
idx = 0
for key in plugin_list:
plugin = plugin_list[key]
reply_str += "#{} {}:\n{}\nv{}\n作者: {}\n".format((idx+1), plugin['name'], plugin['description'],
plugin['version'], plugin['author'])
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
reply_str += "源码: "+updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))+"\n"
reply_str += "\n"
idx += 1
reply = [reply_str]
elif params[0] == 'update':
# 更新所有插件
if is_admin:
def closure():
import pkg.utils.context
updated = []
for key in plugin_list:
plugin = plugin_list[key]
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
success = updater.pull_latest("/".join(plugin['path'].split('/')[:-1]))
if success:
updated.append(plugin['name'])
# 检查是否有requirements.txt
pkg.utils.context.get_qqbot_manager().notify_admin("正在安装依赖...")
for key in plugin_list:
plugin = plugin_list[key]
if os.path.exists("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt"):
logging.info("{}检测到requirements.txt安装依赖".format(plugin['name']))
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.install_requirements("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt")
import main
main.reset_logging()
pkg.utils.context.get_qqbot_manager().notify_admin("[bot]已更新插件: {}".format(", ".join(updated)))
threading.Thread(target=closure).start()
reply = ["[bot]正在更新所有插件,请勿重复发起..."]
else:
reply = ["[bot]err:权限不足"]
elif params[0].startswith("http"):
if is_admin:
def closure():
try:
plugin_host.install_plugin(params[0])
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 指令重载插件")
except Exception as e:
logging.error("插件安装失败:{}".format(e))
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
threading.Thread(target=closure, args=()).start()
reply = ["[bot]正在安装插件..."]
else:
reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
return reply
def process_command(session_name: str, text_message: str, mgr, config,
launcher_type: str, launcher_id: int, sender_id: int) -> list:
reply = []
try:
logging.info(
"[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"..." if len(text_message) > 20 else "")))
cmd = text_message[1:].strip().split(' ')[0]
params = text_message[1:].strip().split(' ')[1:]
if cmd == 'help':
reply = ["[bot]" + config.help_message]
elif cmd == 'reset':
pkg.openai.session.get_session(session_name).reset(explicit=True)
reply = ["[bot]会话已重置"]
elif cmd == 'last':
result = pkg.openai.session.get_session(session_name).last_session()
if result is None:
reply = ["[bot]没有前一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(
datetime_str) + result.prompt[
:min(100,
len(result.prompt))] + \
("..." if len(result.prompt) > 100 else "#END#")]
elif cmd == 'next':
result = pkg.openai.session.get_session(session_name).next_session()
if result is None:
reply = ["[bot]没有后一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(
datetime_str) + result.prompt[
:min(100,
len(result.prompt))] + \
("..." if len(result.prompt) > 100 else "#END#")]
elif cmd == 'prompt':
reply = ["[bot]当前对话所有内容:\n" + pkg.openai.session.get_session(session_name).prompt]
elif cmd == 'list':
pkg.openai.session.get_session(session_name).persistence()
page = 0
if len(params) > 0:
try:
page = int(params[0])
except ValueError:
pass
results = pkg.openai.session.get_session(session_name).list_history(page=page)
if len(results) == 0:
reply = ["[bot]第{}页没有历史会话".format(page)]
else:
reply_str = "[bot]历史会话 第{}页:\n".format(page)
current = -1
for i in range(len(results)):
# 时间(使用create_timestamp转换) 序号 部分内容
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
results[i]['prompt'][
:min(20, len(results[i]['prompt']))])
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
session_name).create_timestamp:
current = i + page * 10
reply_str += "\n以上信息倒序排列"
if current != -1:
reply_str += ",当前会话是 #{}\n".format(current)
else:
reply_str += ",当前处于全新会话或不在此页"
reply = [reply_str]
elif cmd == 'resend':
session = pkg.openai.session.get_session(session_name)
to_send = session.undo()
reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
launcher_type, launcher_id, sender_id)
elif cmd == 'usage':
reply_str = "[bot]各api-key使用情况:\n\n"
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
for key_name in api_keys:
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
.get_text_length_of_key(api_keys[key_name])
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
.get_image_count_of_key(api_keys[key_name])
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
int(image_count))
reply = [reply_str]
elif cmd == 'draw':
if len(params) == 0:
reply = ["[bot]err:请输入图片描述文字"]
else:
session = pkg.openai.session.get_session(session_name)
res = session.draw_image(" ".join(params))
logging.debug("draw_image result:{}".format(res))
reply = [Image(url=res['data'][0]['url'])]
if not (hasattr(config, 'include_image_description')
and not config.include_image_description):
reply.append(" ".join(params))
elif cmd == 'version':
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
try:
if pkg.utils.updater.is_new_version_available():
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
except:
pass
reply = [reply_str]
elif cmd == 'plugin':
reply = plugin_operation(cmd, params, True
if (launcher_type == 'person' and launcher_id == config.admin_qq)
else False)
elif cmd == 'reload' and launcher_type == 'person' and launcher_id == config.admin_qq:
def reload_task():
pkg.utils.reloader.reload_all()
threading.Thread(target=reload_task, daemon=True).start()
elif cmd == 'update' and launcher_type == 'person' and launcher_id == config.admin_qq:
def update_task():
try:
if pkg.utils.updater.update_all():
pkg.utils.reloader.reload_all(notify=False)
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
else:
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
except Exception as e0:
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
return
threading.Thread(target=update_task, daemon=True).start()
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
elif cmd == 'cfg' and launcher_type == 'person' and launcher_id == config.admin_qq:
reply = config_operation(cmd, params)
else:
if cmd.startswith("~") and launcher_type == 'person' and launcher_id == config.admin_qq:
config_item = cmd[1:]
params = [config_item] + params
reply = config_operation("cfg", params)
else:
reply = ["[bot]err:未知的指令或权限不足: " + cmd]
except Exception as e:
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
logging.exception(e)
reply = ["[bot]err:{}".format(e)]
return reply

View File

@ -16,6 +16,9 @@ import pkg.qqbot.filter
import pkg.qqbot.process as processor
import pkg.utils.context
import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
# 并行运行
def go(func, args=()):
@ -51,7 +54,7 @@ def check_response_rule(text: str) -> (bool, str):
class QQBotManager:
retry = 3
bot = None
bot: Mirai = None
reply_filter = None
@ -95,15 +98,64 @@ class QQBotManager:
# Caution: 注册新的事件处理器之后请务必在unsubscribe_all中编写相应的取消订阅代码
@self.bot.on(FriendMessage)
async def on_friend_message(event: FriendMessage):
go(self.on_person_message, (event,))
def friend_message_handler(event: FriendMessage):
# 触发事件
args = {
"launcher_type": "person",
"launcher_id": event.sender.id,
"sender_id": event.sender.id,
"message_chain": event.message_chain,
}
plugin_event = plugin_host.emit(plugin_models.PersonMessageReceived, **args)
if plugin_event.is_prevented_default():
return
self.on_person_message(event)
go(friend_message_handler, (event,))
@self.bot.on(StrangerMessage)
async def on_stranger_message(event: StrangerMessage):
go(self.on_person_message, (event,))
def stranger_message_handler(event: StrangerMessage):
# 触发事件
args = {
"launcher_type": "person",
"launcher_id": event.sender.id,
"sender_id": event.sender.id,
"message_chain": event.message_chain,
}
plugin_event = plugin_host.emit(plugin_models.PersonMessageReceived, **args)
if plugin_event.is_prevented_default():
return
self.on_person_message(event)
go(stranger_message_handler, (event,))
@self.bot.on(GroupMessage)
async def on_group_message(event: GroupMessage):
go(self.on_group_message, (event,))
def group_message_handler(event: GroupMessage):
# 触发事件
args = {
"launcher_type": "group",
"launcher_id": event.group.id,
"sender_id": event.sender.id,
"message_chain": event.message_chain,
}
plugin_event = plugin_host.emit(plugin_models.GroupMessageReceived, **args)
if plugin_event.is_prevented_default():
return
self.on_group_message(event)
go(group_message_handler, (event,))
def unsubscribe_all():
"""取消所有订阅
@ -155,6 +207,7 @@ class QQBotManager:
# 私聊消息处理
def on_person_message(self, event: MessageEvent):
reply = ''
if event.sender.id == self.bot.qq:
@ -189,6 +242,7 @@ class QQBotManager:
# 群消息处理
def on_group_message(self, event: GroupMessage):
reply = ''
def process(text=None) -> str:

92
pkg/qqbot/message.py Normal file
View File

@ -0,0 +1,92 @@
# 普通消息处理模块
import logging
import openai
import pkg.utils.context
import pkg.openai.session
import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
def process_normal_message(text_message: str, mgr, config, launcher_type: str,
launcher_id: int, sender_id: int) -> list:
session_name = f"{launcher_type}_{launcher_id}"
logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"..." if len(text_message) > 20 else "")))
session = pkg.openai.session.get_session(session_name)
reply = []
while True:
try:
prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else ""
text = session.append(text_message)
# 触发插件事件
args = {
"launcher_type": launcher_type,
"launcher_id": launcher_id,
"sender_id": sender_id,
"session": session,
"prefix": prefix,
"response_text": text
}
event = pkg.plugin.host.emit(plugin_models.NormalMessageResponded, **args)
if event.get_return_value("prefix") is not None:
prefix = event.get_return_value("prefix")
if event.get_return_value("reply") is not None:
reply = event.get_return_value("reply")
if not event.is_prevented_default():
reply = [prefix + text]
except openai.error.APIConnectionError as e:
mgr.notify_admin("{}会话调用API失败:{}".format(session_name, e))
reply = ["[bot]err:调用API失败请重试或联系作者或等待修复"]
except openai.error.RateLimitError as e:
logging.debug(type(e))
# 尝试切换api-key
current_key_name = pkg.utils.context.get_openai_manager().key_mgr.get_key_name(
pkg.utils.context.get_openai_manager().key_mgr.using_key
)
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
# 触发插件事件
args = {
'key_name': current_key_name,
'usage': pkg.utils.context.get_openai_manager().audit_mgr
.get_usage(pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()),
'exceeded_keys': pkg.utils.context.get_openai_manager().key_mgr.exceeded,
}
event = plugin_host.emit(plugin_models.KeyExceeded, **args)
if not event.is_prevented_default():
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
if not switched:
mgr.notify_admin(
"api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key".format(
current_key_name))
reply = ["[bot]err:API调用额度超额请联系作者或等待修复"]
else:
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name))
reply = ["[bot]err:API调用额度超额已自动切换请重新发送消息"]
continue
except openai.error.InvalidRequestError as e:
mgr.notify_admin("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或"
"completion_api_params中的max_tokens参数数值过大导致的请尝试将其降低".format(
session_name, e))
reply = ["[bot]err:API调用参数错误请联系作者或等待修复"]
except openai.error.ServiceUnavailableError as e:
# mgr.notify_admin("{}API调用服务不可用:{}".format(session_name, e))
reply = ["[bot]err:API调用服务暂不可用请尝试重试"]
except Exception as e:
logging.exception(e)
reply = ["[bot]err:{}".format(e)]
break
return reply

View File

@ -1,14 +1,10 @@
# 此模块提供了消息处理的具体逻辑的接口
import asyncio
import datetime
import json
import threading
from func_timeout import func_set_timeout
import logging
import openai
from mirai import Image, MessageChain, Plain
from mirai import MessageChain, Plain
# 这里不使用动态引入config
# 因为在这里动态引入会卡死程序
@ -20,76 +16,15 @@ import pkg.openai.manager
import pkg.utils.reloader
import pkg.utils.updater
import pkg.utils.context
import pkg.qqbot.message
import pkg.qqbot.command
import pkg.plugin.host as plugin_host
import pkg.plugin.models as plugin_models
processing = []
def config_operation(cmd, params):
reply = []
config = pkg.utils.context.get_config()
reply_str = ""
if len(params) == 0:
reply = ["[bot]err:请输入配置项"]
else:
cfg_name = params[0]
if cfg_name == 'all':
reply_str = "[bot]所有配置项:\n\n"
for cfg in dir(config):
if not cfg.startswith('__') and not cfg == 'logging':
# 根据配置项类型进行格式化如果是字典则转换为json并格式化
if isinstance(getattr(config, cfg), str):
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
elif isinstance(getattr(config, cfg), dict):
# 不进行unicode转义并格式化
reply_str += "{}: {}\n".format(cfg,
json.dumps(getattr(config, cfg),
ensure_ascii=False, indent=4))
else:
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
reply = [reply_str]
elif cfg_name in dir(config):
if len(params) == 1:
# 按照配置项类型进行格式化
if isinstance(getattr(config, cfg_name), str):
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
elif isinstance(getattr(config, cfg_name), dict):
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
json.dumps(getattr(config, cfg_name),
ensure_ascii=False, indent=4))
else:
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
reply = [reply_str]
else:
cfg_value = " ".join(params[1:])
# 类型转换如果是json则转换为字典
if cfg_value == 'true':
cfg_value = True
elif cfg_value == 'false':
cfg_value = False
elif cfg_value.isdigit():
cfg_value = int(cfg_value)
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
cfg_value = json.loads(cfg_value)
else:
try:
cfg_value = float(cfg_value)
except ValueError:
pass
# 检查类型是否匹配
if isinstance(getattr(config, cfg_name), type(cfg_value)):
setattr(config, cfg_name, cfg_value)
pkg.utils.context.set_config(config)
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
else:
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
else:
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
return reply
@func_set_timeout(config_init_import.process_message_timeout)
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: MessageChain,
sender_id: int) -> MessageChain:
@ -120,210 +55,64 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
pkg.openai.session.get_session(session_name).acquire_response_lock()
# 处理消息
try:
if session_name in processing:
pkg.openai.session.get_session(session_name).release_response_lock()
return MessageChain([Plain("[bot]err:正在处理中,请稍后再试")])
processing.append(session_name)
config = pkg.utils.context.get_config()
is_message = True
processing.append(session_name)
try:
if text_message.startswith('!') or text_message.startswith(""): # 指令
is_message = False
try:
logging.info(
"[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"..." if len(text_message) > 20 else "")))
# 触发插件事件
args = {
'launcher_type': launcher_type,
'launcher_id': launcher_id,
'sender_id': sender_id,
'command': text_message[1:].strip().split(' ')[0],
'params': text_message[1:].strip().split(' ')[1:],
'text_message': text_message,
'is_admin': sender_id is config.admin_qq,
}
event = plugin_host.emit(plugin_models.PersonCommandSent
if launcher_type == 'person'
else plugin_models.GroupCommandSent, **args)
cmd = text_message[1:].strip().split(' ')[0]
if event.get_return_value("alter") is not None:
text_message = event.get_return_value("alter")
params = text_message[1:].strip().split(' ')[1:]
if cmd == 'help':
reply = ["[bot]" + config.help_message]
elif cmd == 'reset':
pkg.openai.session.get_session(session_name).reset(explicit=True)
reply = ["[bot]会话已重置"]
elif cmd == 'last':
result = pkg.openai.session.get_session(session_name).last_session()
if result is None:
reply = ["[bot]没有前一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(
datetime_str) + result.prompt[
:min(100,
len(result.prompt))] + \
("..." if len(result.prompt) > 100 else "#END#")]
elif cmd == 'next':
result = pkg.openai.session.get_session(session_name).next_session()
if result is None:
reply = ["[bot]没有后一次的对话"]
else:
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
'%Y-%m-%d %H:%M:%S')
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(
datetime_str) + result.prompt[
:min(100,
len(result.prompt))] + \
("..." if len(result.prompt) > 100 else "#END#")]
elif cmd == 'prompt':
reply = ["[bot]当前对话所有内容:\n" + pkg.openai.session.get_session(session_name).prompt]
elif cmd == 'list':
pkg.openai.session.get_session(session_name).persistence()
page = 0
# 取出插件提交的返回值赋值给reply
if event.get_return_value("reply") is not None:
reply = event.get_return_value("reply")
if len(params) > 0:
try:
page = int(params[0])
except ValueError:
pass
if not event.is_prevented_default():
reply = pkg.qqbot.command.process_command(session_name, text_message,
mgr, config, launcher_type, launcher_id, sender_id)
results = pkg.openai.session.get_session(session_name).list_history(page=page)
if len(results) == 0:
reply = ["[bot]第{}页没有历史会话".format(page)]
else:
reply_str = "[bot]历史会话 第{}页:\n".format(page)
current = -1
for i in range(len(results)):
# 时间(使用create_timestamp转换) 序号 部分内容
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
results[i]['prompt'][
:min(20, len(results[i]['prompt']))])
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
session_name).create_timestamp:
current = i + page * 10
else: # 消息
# 触发插件事件
args = {
"launcher_type": launcher_type,
"launcher_id": launcher_id,
"sender_id": sender_id,
"text_message": text_message,
}
event = plugin_host.emit(plugin_models.PersonNormalMessageReceived
if launcher_type == 'person'
else plugin_models.GroupNormalMessageReceived, **args)
reply_str += "\n以上信息倒序排列"
if current != -1:
reply_str += ",当前会话是 #{}\n".format(current)
else:
reply_str += ",当前处于全新会话或不在此页"
if event.get_return_value("alter") is not None:
text_message = event.get_return_value("alter")
reply = [reply_str]
elif cmd == 'resend':
session = pkg.openai.session.get_session(session_name)
to_send = session.undo()
text_message = to_send
is_message = True
elif cmd == 'usage':
reply_str = "[bot]各api-key使用情况:\n\n"
# 取出插件提交的返回值赋值给reply
if event.get_return_value("reply") is not None:
reply = event.get_return_value("reply")
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
for key_name in api_keys:
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
.get_text_length_of_key(api_keys[key_name])
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
.get_image_count_of_key(api_keys[key_name])
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
int(image_count))
reply = [reply_str]
elif cmd == 'draw':
if len(params) == 0:
reply = ["[bot]err:请输入图片描述文字"]
else:
session = pkg.openai.session.get_session(session_name)
res = session.draw_image(" ".join(params))
logging.debug("draw_image result:{}".format(res))
reply = [Image(url=res['data'][0]['url'])]
if not (hasattr(config, 'include_image_description')
and not config.include_image_description):
reply.append(" ".join(params))
elif cmd == 'version':
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
try:
if pkg.utils.updater.is_new_version_available():
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
except:
pass
reply = [reply_str]
elif cmd == 'reload' and launcher_type == 'person' and launcher_id == config.admin_qq:
def reload_task():
pkg.utils.reloader.reload_all()
threading.Thread(target=reload_task, daemon=True).start()
elif cmd == 'update' and launcher_type == 'person' and launcher_id == config.admin_qq:
def update_task():
try:
if pkg.utils.updater.update_all():
pkg.utils.reloader.reload_all(notify=False)
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
else:
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
except Exception as e0:
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
return
threading.Thread(target=update_task, daemon=True).start()
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
elif cmd == 'cfg' and launcher_type == 'person' and launcher_id == config.admin_qq:
reply = config_operation(cmd, params)
else:
if cmd.startswith("~") and launcher_type == 'person' and launcher_id == config.admin_qq:
config_item = cmd[1:]
params = [config_item] + params
reply = config_operation("cfg", params)
else:
reply = ["[bot]err:未知的指令或权限不足: " + cmd]
except Exception as e:
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
logging.exception(e)
reply = ["[bot]err:{}".format(e)]
if is_message: # 消息
logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + (
"..." if len(text_message) > 20 else "")))
session = pkg.openai.session.get_session(session_name)
while True:
try:
prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else ""
reply = [prefix + session.append(text_message)]
except openai.error.APIConnectionError as e:
mgr.notify_admin("{}会话调用API失败:{}".format(session_name, e))
reply = ["[bot]err:调用API失败请重试或联系作者或等待修复"]
except openai.error.RateLimitError as e:
logging.debug(type(e))
# 尝试切换api-key
current_key_name = pkg.utils.context.get_openai_manager().key_mgr.get_key_name(
pkg.utils.context.get_openai_manager().key_mgr.using_key
)
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
if not switched:
mgr.notify_admin("api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key".format(
current_key_name))
reply = ["[bot]err:API调用额度超额请联系作者或等待修复"]
else:
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name))
reply = ["[bot]err:API调用额度超额已自动切换请重新发送消息"]
continue
except openai.error.InvalidRequestError as e:
mgr.notify_admin("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或"
"completion_api_params中的max_tokens参数数值过大导致的请尝试将其降低".format(
session_name, e))
reply = ["[bot]err:API调用参数错误请联系作者或等待修复"]
except openai.error.ServiceUnavailableError as e:
# mgr.notify_admin("{}API调用服务不可用:{}".format(session_name, e))
reply = ["[bot]err:API调用服务暂不可用请尝试重试"]
except Exception as e:
logging.exception(e)
reply = ["[bot]err:{}".format(e)]
break
if not event.is_prevented_default():
reply = pkg.qqbot.message.process_normal_message(text_message,
mgr, config, launcher_type, launcher_id, sender_id)
if reply is not None and type(reply[0]) == str:
logging.info(

View File

@ -6,6 +6,7 @@ context = {
},
'logger_handler': None,
'config': None,
'plugin_host': None,
}
@ -38,4 +39,12 @@ def set_qqbot_manager(inst):
def get_qqbot_manager():
return context['inst']['qqbot.manager.QQBotManager']
return context['inst']['qqbot.manager.QQBotManager']
def set_plugin_host(inst):
context['plugin_host'] = inst
def get_plugin_host():
return context['plugin_host']

31
pkg/utils/pkgmgr.py Normal file
View File

@ -0,0 +1,31 @@
from pip._internal import main as pipmain
def install(package):
pipmain(['install', package])
def install_requirements(file):
pipmain(['install', '-r', file])
def ensure_dulwich():
# 尝试三次
for i in range(3):
try:
import dulwich
return
except ImportError:
install('dulwich')
raise ImportError("无法自动安装dulwich库")
if __name__ == "__main__":
try:
install("openai11")
except Exception as e:
print(111)
print(e)
print(222)

View File

@ -4,6 +4,7 @@ import threading
import importlib
import pkgutil
import pkg.utils.context
import pkg.plugin.host
def walk(module, prefix=''):
@ -34,6 +35,10 @@ def reload_all(notify=True):
importlib.reload(__import__('banlist'))
pkg.utils.context.context = context
# 重载插件
import plugins
walk(plugins)
# 执行启动流程
logging.info("执行程序启动流程")
threading.Thread(target=main.main, args=(False,), daemon=False).start()

View File

@ -3,12 +3,35 @@ import datetime
import pkg.utils.context
def update_all() -> bool:
"""使用dulwich更新源码"""
def check_dulwich_closure():
try:
import pkg.utils.pkgmgr
pkg.utils.pkgmgr.ensure_dulwich()
except:
pass
try:
import dulwich
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
def pull_latest(repo_path: str) -> bool:
"""拉取最新代码"""
check_dulwich_closure()
from dulwich import porcelain
repo = porcelain.open_repo(repo_path)
porcelain.pull(repo)
return True
def update_all() -> bool:
"""使用dulwich更新源码"""
check_dulwich_closure()
import dulwich
try:
before_commit_id = get_current_commit_id()
from dulwich import porcelain
@ -35,12 +58,30 @@ def update_all() -> bool:
raise Exception("分支不一致,自动更新仅支持master分支,请手动更新(https://github.com/RockChinQ/QChatGPT/issues/76)")
def is_repo(path: str) -> bool:
"""检查是否是git仓库"""
check_dulwich_closure()
from dulwich import porcelain
try:
porcelain.open_repo(path)
return True
except:
return False
def get_remote_url(repo_path: str) -> str:
"""获取远程仓库地址"""
check_dulwich_closure()
from dulwich import porcelain
repo = porcelain.open_repo(repo_path)
return str(porcelain.get_remote_repo(repo, "origin")[1])
def get_current_version_info() -> str:
"""获取当前版本信息"""
try:
import dulwich
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
check_dulwich_closure()
from dulwich import porcelain
@ -62,10 +103,7 @@ def get_current_version_info() -> str:
def get_commit_id_and_time_and_msg() -> str:
"""获取当前提交id和时间和提交信息"""
try:
import dulwich
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
check_dulwich_closure()
from dulwich import porcelain
@ -79,10 +117,7 @@ def get_commit_id_and_time_and_msg() -> str:
def get_current_commit_id() -> str:
"""检查是否有新版本"""
try:
import dulwich
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
check_dulwich_closure()
from dulwich import porcelain
@ -97,10 +132,7 @@ def get_current_commit_id() -> str:
def is_new_version_available() -> bool:
"""检查是否有新版本"""
try:
import dulwich
except ModuleNotFoundError:
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
check_dulwich_closure()
from dulwich import porcelain

14
plugins/__init__.py Normal file
View File

@ -0,0 +1,14 @@
# 在此处填写的插件仓库将会被自动下载并加载
# 支持gitee和github仓库
# 这种加载插件的方式是推荐的,便于插件的获取和更新
#
# 示例:
# plugin_repos = [
# 'https://github.com/SampleUser/SampleRepo',
# 'https://gitee.com/SampleUser/SampleRepo'
# ]
remote_repos = [
]

BIN
res/plugin_hello_group.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
res/plugin_hello_person.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -0,0 +1,3 @@
# 插件示例
# 将此目录下的目录放入plugins目录即可使用
# 每个示例插件的功能请查看其包内的__init__.py或README.md

View File

View File

@ -0,0 +1,51 @@
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost
"""
基本命令的中文形式支持
"""
__mapping__ = {
"帮助": "help",
"重置": "reset",
"前一次": "last",
"后一次": "next",
"会话内容": "prompt",
"列出会话": "list",
"重新回答": "resend",
"使用量": "usage",
"绘画": "draw",
"版本": "version",
"热重载": "reload",
"热更新": "update",
"配置": "cfg",
}
@register(name="CmdCN", description="命令中文支持", version="0.1", author="RockChinQ")
class CmdCnPlugin(Plugin):
def __init__(self, plugin_host: PluginHost):
pass
# 私聊发送指令
@on(PersonCommandSent)
def person_command_sent(self, event: EventContext, **kwargs):
cmd = kwargs['command']
if cmd in __mapping__:
# 返回替换后的指令
event.add_return("alter", "!"+__mapping__[cmd]+" "+" ".join(kwargs['params']))
# 群聊发送指令
@on(GroupCommandSent)
def group_command_sent(self, event: EventContext, **kwargs):
cmd = kwargs['command']
if cmd in __mapping__:
# 返回替换后的指令
event.add_return("alter", "!"+__mapping__[cmd]+" "+" ".join(kwargs['params']))
def __del__(self):
pass

View File

@ -0,0 +1,50 @@
from pkg.plugin.models import *
from pkg.plugin.host import EventContext, PluginHost
"""
在收到私聊或群聊消息"hello"回复"hello, <发送者id>!""hello, everyone!"
"""
# 注册插件
@register(name="Hello", description="hello world", version="0.1", author="RockChinQ")
class HelloPlugin(Plugin):
# 插件加载时触发
# plugin_host (pkg.plugin.host.PluginHost) 提供了与主程序交互的一些方法,详细请查看其源码
def __init__(self, plugin_host: PluginHost):
pass
# 当收到个人消息时触发
@on(PersonNormalMessageReceived)
def person_normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
if msg == "hello": # 如果消息为hello
# 输出调试信息
logging.debug("hello, {}".format(kwargs['sender_id']))
# 回复消息 "hello, <发送者id>!"
event.add_return("reply", ["hello, {}!".format(kwargs['sender_id'])])
# 阻止该事件默认行为(向接口获取回复)
event.prevent_default()
# 当收到群消息时触发
@on(GroupNormalMessageReceived)
def group_normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
if msg == "hello": # 如果消息为hello
# 输出调试信息
logging.debug("hello, {}".format(kwargs['sender_id']))
# 回复消息 "hello, everyone!"
event.add_return("reply", ["hello, everyone!"])
# 阻止该事件默认行为(向接口获取回复)
event.prevent_default()
# 插件卸载时触发
def __del__(self):
pass

View File

@ -0,0 +1,43 @@
import random
from mirai import Plain
from pkg.plugin.models import *
from pkg.plugin.host import EventContext
"""
私聊或群聊消息为以下列出的一些冒犯性词语时自动回复__random_reply__中的一句话
"""
__words__ = ['sb', "傻逼", "dinner", "操你妈", "cnm", "fuck you", "fuckyou",
"f*ck you", "弱智", "若智", "答辩", "依托答辩", "低能儿", "nt", "脑瘫", "闹谈", "老坛"]
__random_reply__ = ['好好好', "啊对对对", "好好好好", "你说得对", "谢谢夸奖"]
@register(name="啊对对对", description="你都这样了,我就顺从你吧", version="0.1", author="RockChinQ")
class AdddPlugin(Plugin):
def __init__(self, plugin_host: PluginHost):
pass
# 绑定私聊消息事件和群消息事件
@on(PersonNormalMessageReceived)
@on(GroupNormalMessageReceived)
def normal_message_received(self, event: EventContext, **kwargs):
msg = kwargs['text_message']
# 如果消息中包含关键词
if msg in __words__:
# 随机一个回复
idx = random.randint(0, len(__random_reply__)-1)
# 返回回复的消息
event.add_return("reply", [Plain(__random_reply__[idx])])
# 阻止向接口获取回复
event.prevent_default()
def __del__(self):
pass